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

时间:2013-10-10 13:20:35

标签: ios objective-c cocoa sqlite core-data

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

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

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

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

所有这些都引出了我的主要问题,即什么可能出错,如果我以这种方式更改数据库,而不立即告诉主要上下文合并更改?我假设并不是所有的阳光都是玫瑰。

我脑子里想到的一个特定情况是,如果在主要上下文中加载的对象需要满足错误,如果后台操作之间有从数据库中删除该对象,会发生什么?例如,这可能发生在基于NSFetchedResultsController的表视图上,该视图使用batchSize逐步将对象提取到内存中吗?即,一个尚未完全获取的对象被删除但是我们向上滚动到需要加载对象的点。这是一个潜在的问题吗?其他事情可能会出错吗?我对此事的任何意见表示感谢。

4 个答案:

答案 0 :(得分:5)

很棒的问题!

  

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

不幸的是它会引起问题。将抛出以下异常:

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

This blog post(标题为“如何与Core Data进行并发?”一节)可能有些帮助,但它并没有用尽这个主题。我正在努力解决我现在正在处理的应用程序中的相同问题,并希望阅读有关它的文章。

答案 1 :(得分:3)

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

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

这种方法会遇到一些问题。 - NSPersistentStoreCoordinator's job用于在其附加的 NSManagedObjectContexts及其附加商店之间进行调解。在您描述的多协调器上下文场景中,NSPersistentStoreCoordinator 2及其上下文将不会看到NSManagedObjectContext 1对底层存储的更改导致SQLite文件发生更改。 2不知道1改变了文件,你将有&#34;无法履行错误&#34;和其他令人兴奋的例外 - 在某些时候,您仍然必须将已更改的NSManagedObjects从导入到NSManagedObjectContext 2.如果这些更改很大,您仍然会遇到UI问题,并且UI将与商店不同步,可能导致& #34;无法履行错误&#34;。 - 通常,因为NSManagedObjectContext 2没有使用与NSManagedObjectContext 1相同的NSPersistentStoreCoordinator,所以您将遇到不同步的问题。这并不意味着这些东西是如何一起使用的。如果导入并保存在NSManagedObjectContext 1中,NSManagedObjectContext 2将立即处于与商店不一致的状态。

这些方法可能会出现问题。在解决故障时,大多数这些问题都会变得可见,因为它会访问商店。您可以在Core Data Programming Guide中详细了解此过程的工作原理,而Incremental Store Programming Guide则详细介绍该过程。 SQLite存储遵循增量存储实现所执行的相同过程。

同样,您正在描述的用例 - 获取大量新数据,对数据执行find-Or-Create以创建或更新托管对象,以及删除&#34;陈旧&#34;实际上可能是商店大多数的商品 - 这是我每天都要处理好几年的事情,看到你遇到的所有问题。有解决方案 - 即使是一次更改60,000个复杂对象的导入,甚至使用线程限制! - 但这超出了你的问题的范围。 (提示:父子情境不需要合并通知)。

答案 2 :(得分:3)

两个持久存储协调器(pscs)当然是大型数据集的一种方式。文件锁定比核心数据中的锁定更快。

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

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

我参与了大规模的核心数据项目,并且这种模式对我来说非常有用。

答案 3 :(得分:1)

实际上,这是您可以使用的最佳核心数据方案。几乎没有主UI过时,并且您的数据易于后台管理。当你想告诉主要上下文(也许是当前正在运行的NSFetchedResultsController)时,你会监听backgroundContext的保存通知,如下所示:

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

然后,您可以合并更改,但在保存之前等待主线程上下文捕获它们。收到mergeChangesFromContextDidSaveNotification通知后,更改尚未保存。因此performBlockAndWait是必需的,因此Main上下文获取更改,然后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];
        }];
    }
}

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