使用两个持久存储协调器进行高效后台更新的陷阱

Posted

技术标签:

【中文标题】使用两个持久存储协调器进行高效后台更新的陷阱【英文标题】:Pitfalls of using two persistent store coordinators for efficient background updates 【发布时间】:2013-10-18 06:45:00 【问题描述】:

我正在寻找在后台更新相当大的基于核心数据的数据集的最佳方法,同时尽可能减少对应用程序 UI(主线程)的影响。

有一些关于这个主题的好材料,包括:

Session 211 from WWDC 2013(核心数据性能优化和调试,25:30左右开始) Importing Large Data Sets from objc.io Common Background Practices from objc.io(后台核心数据) Backstage with Nested Managed Object Contexts

根据我的研究和个人经验,最好的选择是有效地使用两个单独的核心数据堆栈,它们仅在数据库 (SQLite) 级别共享数据。这意味着我们需要两个独立的NSPersistentStoreCoordinators,每个都有自己的NSManagedObjectContext。在数据库上启用预写日志记录(从 ios 7 开始默认),几乎所有情况下都可以避免锁定需求(除非我们有两个或更多同时写入,这在我的场景中不太可能)。

为了进行高效的后台更新并节省内存,还需要批量处理数据并定期保存后台上下文,以便将脏对象存储到数据库并从内存中刷新。可以使用此时生成的NSManagedObjectContextDidSaveNotification 将背景更改合并到主上下文中,但通常您不希望在保存批次后立即更新您的 UI。您希望等到后台作业完全完成后再刷新 UI(在 WWDC 会话和 objc.io 文章中都推荐)。这实际上意味着应用程序主上下文在一段时间内与数据库保持不同步。

所有这一切都让我想到了我的主要问题,即,如果我以这种方式更改数据库,而不立即告诉主要上下文合并更改,会出现什么问题?我假设不是所有的阳光和玫瑰。

我脑海中的一个特定场景是,如果需要为在主上下文中加载的对象执行故障,如果后台操作介于两者之间从数据库中删除了该对象,会发生什么?例如,这是否会发生在基于 NSFetchedResultsController 的表视图上,该表视图使用 batchSize 以增量方式将对象提取到内存中?即,尚未完全获取的对象被删除,但我们向上滚动到需要加载对象的点。这是一个潜在的问题吗?其他事情会出错吗?我将不胜感激有关此事的任何意见。

【问题讨论】:

具体来说,您现在在导入过程中在哪里发现性能问题? Instruments 中的 Core Data 探针告诉您什么?我每天在我的一个应用程序中处理一个非常相似的场景,在 UI 处于活动状态时导入和合并一个非常大的数据集。它可以完成,并且可以在没有多个 PSC 或存储的情况下高效完成。 这不是导入,而是更新,这意味着现有数据可以更改(包括被删除)。性能问题与我喜欢的文章和演讲中描述的差不多,实际上甚至不是这里的主要问题。我已经有一个仅使用后台上下文的解决方案(与新的 PSC 相比),并且对主线程的性能影响还不错。但即使这样也不能阻止我上面概述的问题。如果我批量保存后台上下文并且在批处理完成数据库时不立即合并更改,并且 UI 上下文不同步。 好的,你是使用一个 NSPersistentStore 还是两个? 一个 NSPersistentStore。 【参考方案1】:

好问题!

即,尚未完全提取的对象被删除,但 比我们向上滚动到需要加载对象的点。是 这是一个潜在的问题?

不幸的是,它会导致问题。将抛出以下异常:

Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xc544570 <x-coredata://(...)>'

This blog post(标题为“如何使用 Core Data 进行并发?”的部分)可能会有所帮助,但并没有穷尽这个主题。我正在为我正在开发的应用程序中遇到同样的问题而苦苦挣扎,并且很想阅读有关它的文章。

【讨论】:

谢谢。我也看过那篇文章。那里给出的建议,翻译成我的示例案例基本上意味着,你不应该触摸后台作业中的任何对象,如果它们可以被 feted 结果控制器获取。不太理想。 如果在后台上下文中删除了某些对象,我不确定是否可以通过其他方式避免此问题,而不是立即合并。如果您偶然发现更好的解决方案,请随时通知我。 实际上,如果“背景”上下文是驱动 NSFetchedResultsController 的上下文的子上下文,那么在将子级的更改推送到父级之前就可以了。 @quellish:它们不能嵌套,因为它们来自不同的持久存储协调器:This means that we need two separate NSPersistentStoreCoordinators, each of them having it's own NSManagedObjectContext. 【参考方案2】:

根据您的问题、cmets 和我自己的经验,您要解决的更大问题似乎是: 1. 在主线程上使用 NSFetchedResultsController 并进行线程限制 2. 导入大型数据集,将在上下文中插入、更新或删除托管对象。 3. 导入导致大型合并通知由主线程处理以更新UI。 4.大合并有几种可能的影响: - 用户界面变慢,或太忙而无法使用。这可能是因为您正在使用beginUpdates/endUpdates 更新NSFetchedResultsControllerDelegate 中的表格视图,并且由于大合并,您有很多动画排队。 - 用户在尝试访问已从商店中删除的故障对象时可能会遇到“无法完成故障”。托管对象上下文认为它存在,但是当它去存储完成故障时,它已经被删除了。如果你使用reloadData 来更新你的 NSFetchedResultsControllerDelegate 中的 tableview,你会比使用 beginUpdates/endUpdates 更有可能看到这种情况。

您尝试用于解决上述问题的方法是: - 创建两个 NSPersistentStoreCoordinators,每个都附加到相同的 NSPersistentStore 或至少相同的 NSPersistentStore SQLite 存储文件 URL。 - 您的导入发生在 NSManagedObjectContext 1 上,附加到 NSPersistentStoreCoordinator 1,并在其他一些线程上执行。您的 NSFetchedResultsController 正在使用 NSManagedObjectContext 2,附加到 NSPersistentStoreCoordinator 2,在主线程上运行。 - 您正在将更改从 NSManagedObjectContext 1 移动到 2

使用这种方法会遇到一些问题。 - NSPersistentStoreCoordinator's job 在它的附加 NSManagedObjectContexts 和它的附加存储之间进行调解。在您描述的多协调器上下文场景中,NSManagedObjectContext 1 对底层存储的更改会导致 SQLite 文件发生更改,而 NSPersistentStoreCoordinator 2 及其上下文将不会看到。 2 不知道 1 改变了文件,你会出现“Could not fulfill fault”和其他令人兴奋的异常。 - 在某些时候,您仍然必须将更改的 NSManagedObjects 从导入中放入 NSManagedObjectContext 2。如果这些更改很大,您仍然会遇到 UI 问题,并且 UI 将与商店不同步,可能导致“无法履行过错”。 - 一般来说,因为 NSManagedObjectContext 2 没有使用与 NSManagedObjectContext 1 相同的 NSPersistentStoreCoordinator,所以你会遇到事情不同步的问题。这不是这些东西打算一起使用的方式。如果在 NSManagedObjectContext 1 中导入保存,NSManagedObjectContext 2 立即处于与 store 不一致的状态。

这些是这种方法可能出错的一些事情。大多数这些问题在触发故障时会变得可见,因为它会访问存储。您可以在Core Data Programming Guide 中阅读有关此过程如何工作的更多信息,而Incremental Store Programming Guide 则更详细地描述了该过程。 SQLite 存储遵循与增量存储实现相同的过程。

再次,您描述的用例 - 获取大量新数据,在数据上执行 find-Or-Create 以创建或更新托管对象,并删除实际上可能是大部分存储的“陈旧”对象 -是我多年来每天都在处理的事情,看到所有与您相同的问题。有一些解决方案 - 甚至对于一次更改 60,000 个复杂对象的导入,甚至使用线程限制! - 但这超出了您的问题范围。 (提示:父子上下文不需要合并通知)。

【讨论】:

感谢您详细说明此事并基本上证实了我的怀疑。父子上下文是最简单的路径,但创建主线程的子上下文确实对 UI 产生了明显的性能影响(请参阅bit.ly/14ManfK)。此外,对于子上下文,您无法真正轻松地阻止在后台保存一批对象后立即发生 UI 更新。为了防止出现故障问题,我正在考虑只删除已删除的对象,而不是立即删除它们。实际删除将发生在 UI 更新之前。 @MatejBukovinski:我不知道你有多少数据,但你可以将实际删除从持久存储移动到applicationDidEnterBackground:。在UI 中,您可以检查isDeleted 标志是否为假(类似的东西在服务器端编程中很常见,但数据通常只是无限期地保存在数据库中)。不确定我现在是不是在重新发明***。 不要使用父子上下文进行后台更新,它们不是为此而设计的。您的背景上下文会阻塞主要上下文,从而导致性能更差。 @malhal 这绝对不正确。没有理由使用主队列上下文,也没有理由对除了管理持久存储的队列之外的任何东西执行阻塞操作。 OP 使用 2 个持久协调器是正确的,这是 Apple 在他们的示例中推荐的:developer.apple.com/library/content/samplecode/Earthquakes/… 他的担忧是对已删除的对象的故障是有效的,这以前会异常但已修复在 iOS 10 中。有趣的是,当他们解决了这个问题时,他们现在还支持使用与主上下文相同的协调器的背景上下文。发生了很多新的事情。【参考方案3】:

两个持久存储协调器 (pscs) 无疑是处理大型数据集的方法。文件锁定比核心数据中的锁定更快。

没有理由不能使用后台 psc 创建线程受限的 NSManagedObjectContexts,其中每个都是为您在后台执行的每个操作创建的。但是,您现在需要创建 NSOperationQueues 和/或线程来根据您在后台执行的操作来管理操作,而不是让核心数据管理队列。 NSManagedObjectContexts 是免费的而且不贵。一旦你这样做了,你就可以挂在你的 NSManagedObjectContext 上,并且只在一个操作和/或线程生命周期内使用它,并根据需要构建尽可能多的更改,然后等到最后提交它们并将它们合并到主线程,无论你怎么做决定。即使您有一些主线程写入,您仍然可以在操作生命周期的关键点重新获取/合并回您的线程上下文。

此外,重要的是要知道,如果您正在处理大量数据,只要您不接触其他内容,就不必担心合并上下文。例如,如果您有 A 类和 B 类,并且您有两个单独的操作/线程来处理它们并且它们没有直接关系,那么您不必合并上下文,如果一个更改可以继续滚动更改。以这种方式合并背景上下文的唯一主要需求是是否存在直接关系故障。最好通过某种序列化来防止这种情况发生,无论是 NSOperationQueue 还是其他任何东西。因此,您可以随意处理背景中的不同对象,但请注意它们之间的关系。

我从事过大型核心数据项目,这种模式对我来说非常有效。

【讨论】:

【参考方案4】:

确实,这是您可以使用的最佳核心数据方案。几乎没有主 UI 陈旧性,并且可以轻松地对数据进行后台管理。当您想告诉主上下文(可能是当前正在运行的NSFetchedResultsController)时,您可以像这样监听 backgroundContext 的保存通知:

    [[NSNotificationCenter defaultCenter] 
      addObserver:self selector:@selector(reloadFetchedResults:)
      name:NSManagedObjectContextDidSaveNotification
      object:backgroundObjectContext];

然后,您可以合并更改,但在保存之前等待主线程上下文捕获它们。当您收到mergeChangesFromContextDidSaveNotification 通知时,尚未保存更改。因此performBlockAndWait 是强制性的,因此主上下文获取更改,然后NSFetchedResultsController 正确更新其值。

-(void)reloadFetchedResults:(NSNotification*)notification

    NSManagedObjectContext*moc=[notification object];
    if ([moc isEqual:backgroundObjectContext]) 
    
        // Delete caches of fethcedResults if you have a deletion
        if ([[theNotification.userInfo objectForKey:NSDeletedObjectsKey] count]) 
            [NSFetchedResultsController deleteCacheWithName:nil];
         
        // Block the background execution of the save, and merge changes before
        [managedObjectContext performBlockandWait:^
            [managedObjectContext 
            mergeChangesFromContextDidSaveNotification:notification];
        ];
    

有一个没有人注意到的陷阱。 您可以在后台上下文实际保存您要合并的对象之前收到保存通知。如果您想通过更快的 Main Context 来避免出现问题,请求后台上下文尚未保存的对象,您应该(您真的应该)调用 obtainPermanentIDsForObjectsbefore强>任何背景保存。然后您可以安全地拨打mergeChangesFromContextDidSaveNotification。这将确保合并收到一个有效的永久 Id 用于合并。

【讨论】:

以上是关于使用两个持久存储协调器进行高效后台更新的陷阱的主要内容,如果未能解决你的问题,请参考以下文章

两个托管对象上下文可以共享一个持久存储协调器吗?

是否可以创建自定义核心数据持久存储协调器?

具有单个持久存储协调器的多个数据模型

持久存储协调器核心数据错误:NSSQLiteErrorDomain = 522

核心数据:无法将持久存储添加到协调器

核心数据持久存储协调器不存在