27

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

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

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

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

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

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

4

4 回答 4

5

好问题!

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

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

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

这篇博文(标题为“如何使用 Core Data 进行并发?”的部分)可能会有所帮助,但并没有穷尽这个主题。我正在努力解决我现在正在开发的应用程序中的相同问题,并且很想阅读有关它的文章。

于 2013-10-10T13:44:23.037 回答
3

根据您的问题、评论和我自己的经验,您要解决的更大问题似乎是:1. 在主线程上使用 NSFetchedResultsController 并限制线程 2. 导入一个大型数据集,它将插入、更新、或删除上下文中的托管对象。3. 导入导致大型合并通知由主线程处理以更新UI。4. 大合并有几个可能的影响: - UI 变慢,或者太忙而无法使用。这可能是因为您正在使用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 的 工作是在它的附加之间进行调解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 立即处于与存储不一致的状态。

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

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

于 2013-10-16T19:58:45.773 回答
3

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

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

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

我从事过大型核心数据项目,并且这种模式对我来说效果很好。

于 2013-10-17T19:24:07.703 回答
1

事实上,这是您可以使用的最佳核心数据方案。几乎没有主 UI 陈旧性,并且可以轻松地对数据进行后台管理。当您想告诉 Main Context (可能是当前正在运行的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];
        }];
    }
}

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

于 2014-08-07T09:47:11.990 回答