【问题标题】:Setup that allows a child NSManagedObjectContext to Fetch when it's parent context is Saving允许子 NSManagedObjectContext 在其父上下文正在保存时获取的设置
【发布时间】:2025-12-25 13:00:11
【问题描述】:

背景

Saving a large amount of data at a time is very slow.

当前设置

在我的应用中有一个 private-queue NSManagedObjectContext 作为 parent 直接与 NSPersistentStoreCoordinator 对话以保存数据。 子主队列上下文由 UI 的 NSTreeController (NSOutlineView) 使用。

(我的目标是防止出现任何沙滩球。目前我通过仅在应用程序处于非活动状态时保存数据来解决此问题。但由于计划删除的数据没有尚未删除,它们可能仍会出现在提取结果中。这是我正在尝试解决的另一个问题。)

问题

子主队列上下文只能在获取时等待当父上下文忙于保存时。

相关问题

当我有更多发现时,我会更新这个问题。

【问题讨论】:

    标签: ios swift macos cocoa core-data


    【解决方案1】:

    我猜你正在为 OS X / macOS (NSTreeController & NSOutlineView) 开发。我没有使用 macOS 的经验——我是为 iOS 开发的——所以你在阅读我的回复时可能需要考虑到这一点。

    我还没有切换到 swift - 我的代码显然是 Objective-C...

    我将从如何准备 Core Data 堆栈开始。

    我在头文件中设置了两个公共属性:

    @property (nonatomic, strong) NSManagedObjectContext *mocPrivate;
    @property (nonatomic, strong) NSManagedObjectContext *mocMain;
    

    虽然这是不必要的,但我也更喜欢为我的 Core Data 对象设置私有属性,例如:

    @property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
    

    一旦我指向了我的模型 URL,建立了我的托管对象模型 NSManagedObjectModel,指向了我的 NSPersistentStore 的商店 URL 并建立了我的持久商店协调器 NSPersistentStoreCoordinator (PSC),我设置了我的两个托管对象上下文 (MOC)。

    在“构建”我的核心数据堆栈的方法中,在完成上述段落的代码后,我将包含以下内容...

    if (!self.mocPrivate) {
        self.mocPrivate = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [self.mocPrivate setPersistentStoreCoordinator:self.persistentStoreCoordinator];
    } else {
        // report to console the use of existing MOC
    }
    
    if (!self.mocMain) {
        self.mocMain = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [self.mocMain setParentContext:self.mocPrivate];
    } else {
        // report to console the use of existing MOC
    }
    

    (我通常在此代码中包含几行 NSLog 行以向我的控制台报告,但我在此处排除了这些行以保持代码清洁。)

    请注意这段代码的两个重要方面...

    • 设置私有队列MOC与PSC交互;和
    • 将主队列 MOC 设置为私有队列 MOC 的子队列。

    为什么要这样做?首先让我们强调几个重点:

    • 保存到内存相对较快;和
    • 保存到光盘的速度相对较慢。

    私有队列与主队列异步。用户界面 (UI) 在主队列上运行。私有队列在“后台”的单独线程上运行,以维护上下文并与 PSC 协调数据持久性,由 Core Data 和 iOS 完美管理。主队列在 UI 的主线程上运行。

    用不同的方式写...

    • 完成不规则(由 Core Data 管理)数据持久性到 PSC(保存到磁盘)的繁重工作在专用队列中完成;和
    • 完成定期(由开发人员管理)数据持久性到 MOC(保存到内存)的轻量工作在主队列中完成。

    理论上,这应该确保您的 UI 永远不会被阻塞。

    但是这个解决方案还有更多。我们如何管理“保存”过程很重要...

    我写了一个公共方法:

    - (void)saveContextAndWait:(BOOL)wait;
    

    我从任何需要持久化数据的类中调用此方法。这个公共方法的代码:

    - (void)saveContextAndWait:(BOOL)wait {
        // 1. First
        if ([self.mocMain hasChanges]) {
        // 2. Second
            [self.mocMain performBlockAndWait:^{
                NSError __autoreleasing *error;
                BOOL success;
                if (!(success = [self.mocMain save:&error])) {
                    // error handling
                } else {
                    // report success to the console
                }
            }];
        } else {
            NSLog(@"%@ - %@ - CORE DATA - reports no changes to managedObjectContext MAIN_", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
        }
    
        // 3. Third
        void (^savePrivate) (void) = ^{
            NSError __autoreleasing *error;
            BOOL success;
            if (!(success = [self.mocPrivate save:&error])) {
                    // error handling
                } else {
                    // report success to the console
            }
        };
    
        // 4. Fourth
        if ([self.mocPrivate hasChanges]) {
        // 5. Fifth
            if (wait) {
                [self.mocPrivate performBlockAndWait:savePrivate];
            } else {
                [self.mocPrivate performBlock:savePrivate];
            }
        } else {
            NSLog(@"%@ - %@ - CORE DATA - reports no changes to managedObjectContext PRIVATE_", NSStringFromClass(self.class), NSStringFromSelector(_cmd));
        }
    }
    

    所以我将通过这个来解释正在发生的事情。

    我为开发人员提供保存和等待(阻止)的选项,并且根据开发人员对方法 saveContextAndWait:wait 的使用,私有队列 MOC 使用以下任一方式“保存”:

    • performBlockAndWait方法(开发者调用方法wait=TRUEYES);或
    • performBlock 方法(开发人员使用wait = FALSENO 调用方法)。

    首先,该方法检查主队列MOC是否有任何变化。除非必须,否则我们不要做任何工作!

    其次,该方法完成对主队列 MOC 上的performBlockAndWait 的(同步)调用。这会在代码块中执行对save 方法的调用,并在允许代码继续之前等待完成。请记住,这是用于定期“保存”小型数据集。这里不需要调用performBlock 的(异步)选项,实际上会破坏该方法的有效性,正如我在我的代码中学习实现它时所经历的那样(由于save 调用而无法持久化数据在私有队列 MOC 上完成 save 后尝试在主队列 MOC 上完成)。

    第三,我们在一个包含保存私有队列 MOC 的代码的块中编写一个小块。

    第四,该方法检查私有队列MOC是否有任何变化。这可能是不必要的,但在这里包含它是无害的。

    第五,根据开发者选择实现的选项(wait = YESNO),该方法在块内的块上调用performBlockAndWaitperformBlock(在第三 以上)。

    在这最后一步,不管实现方式(wait=YESorNO),持久化数据到磁盘的功能,从私有队列MOC到PSC,被抽象到主线程的异步线程上的私有队列。理论上,通过 PSC 的“保存到光盘”可以根据需要进行,因为它与主线程无关。并且因为私有队列 MOC 的所有数据都在内存中,所以主队列 MOC 会完全自动地通知更改,因为它是私有队列 MOC 的子队列。

    如果您将大量数据导入应用程序(我目前正在努力实现),那么将这些数据导入私有队列 MOC 是有意义的。

    私有队列 MOC 在这里做了两件事:

    • 它与 PSC 协调数据持久性(到磁盘);
    • 因为是主队列MOC的父级(在内存中),主队列MOC会被通知私有队列MOC中的数据变化,合并由Core Data管理;

    最后,我使用NSFetchedResultsController (FRC) 来管理我的数据提取,这些都是针对主队列 MOC 完成的。这维护了数据层次结构。在任一上下文中对数据集进行更改时,FRC 都会更新视图。

    这个解决方案很简单! (有一次我花了几周的时间来解决这个问题,又花了几周的时间来完善我的代码。)

    无需监控合并或其他 MOC 更改的通知。 Core Data 和 iOS 在后台处理一切。

    因此,如果这对您不起作用 - 请告诉我 - 我可能在一年多前编写此代码时排除或忽略了某些内容。

    【讨论】:

    • 感谢安德鲁!我的应用程序在以前的版本中具有几乎相同的设置。但是由于数据集很大,每次我保存主队列(子)MOC时都会有一个滚动的沙滩球(通常是在删除了成千上万个对象的数据层次结构之后)。 MOC 需要根据级联删除规则决定删除什么。即使这一切都发生在内存中,它仍然相对较慢(可能 5-7 秒)。其次,当 background-moc 忙于保存时,主队列 moc 无法立即获取数据,因为父级是它从中获取数据的地方。所以我切换到我目前的设置。
    • 您提出的堆栈确实适用于相对少量的数据,并且当用户(在 UI 的意义上)不经常启动提取时。 (当父上下文的队列有未完成的任务时,只要用户不点击任何启动提取的东西,它就不会阻塞 UI。)我将继续调查这个问题。我可以尝试添加第二个 PSC 以供阅读。这样可以解决“写不完”的问题。
    • 也可能以异步方式进行 ftech,并给用户一些视觉提示,让他们知道 UI 稍后会更新,这是一个很好的解决方案。但是如果保存在主队列moc中仍然会有一个沙滩球
    • 有趣的问题。我会担心使用两个 PSC - 因为您仍然会在 SQLite 存储中发生记录级别的锁定(假设您正在使用这)。 Core Data 不是针对一个 PSC 进行了优化吗?但是,如果您将第二个 PSC 用于只读,那么这可能不会成为问题。我的建议是退后一步,看看你想要实现的目标......你的应用程序是否需要同时在屏幕上展示所有已删除记录的意识,或者你是否可以考虑管理数据分批?排除要删除的记录是否有帮助?
    • 关于 Matt Gallagher 的文章和评论“Nothing gets close to NSDictionary”,您是否考虑过将相关的现有数据提取到 NSDictionaryNSArray 并在应用程序是用户时将其存储在内存中面对?然后,您可以管理内存中的所有数据事务。也许更容易获取新数据和删除旧数据?您将失去使用 FRC 的能力,但在您的情况下这可能不是一件坏事?那么一旦用户需求减少,你可以将NSDictionary中的数据集插入到私有队列MOC中,让Core Data管理持久化?