101

问题:如何让我的子上下文看到父上下文中持续存在的更改,以便它们触发我的 NSFetchedResultsController 来更新 UI?

这是设置:

您有一个应用程序,它下载并添加大量 XML 数据(大约 200 万条记录,每条记录大致相当于一段普通文本的大小)。.sqlite 文件的大小约为 500 MB。将此内容添加到 Core Data 需要时间,但您希望用户能够在数据以增量方式加载到数据存储时使用该应用程序。大量数据在四处移动,因此用户必须不可见且无法察觉,因此不会出现挂起和抖动:像黄油一样滚动。尽管如此,应用程序更有用,添加的数据越多,所以我们不能永远等待数据被添加到核心数据存储中。在代码中,这意味着我真的很想在导入代码中避免这样的代码:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

该应用程序仅适用于 iOS 5,因此它需要支持的最慢设备是 iPhone 3GS。

以下是我迄今为止用于开发当前解决方案的资源:

Apple 的核心数据编程指南:高效导入数据

  • 使用自动释放池来降低内存
  • 关系成本。导入平面,然后在最后修补关系
  • 不要询问您是否可以帮助它,它会以 O(n^2) 的方式减慢速度
  • 批量导入:保存、重置、排空和重复
  • 导入时关闭撤消管理器

iDeveloper TV - 核心数据性能

  • 使用 3 个上下文:主上下文类型、主要上下文类型和限制上下文类型

iDeveloper TV - 适用于 Mac、iPhone 和 iPad 的核心数据更新

  • 使用 performBlock 在其他队列上运行保存会使事情变得更快。
  • 加密会减慢速度,如果可以,请关闭它。

Marcus Zarra 在 Core Data 中导入和显示大型数据集

  • 您可以通过为当前的运行循环留出时间来减慢导入速度,从而让用户感觉顺畅。
  • 示例代码证明可以进行大量导入并保持 UI 响应,但不如使用 3 个上下文和异步保存到磁盘快。

我目前的解决方案

我有 3 个 NSManagedObjectContext 实例:

masterManagedObjectContext - 这是具有 NSPersistentStoreCoordinator 并负责保存到磁盘的上下文。我这样做是为了让我的保存可以是异步的,因此非常快。我在启动时创建它,如下所示:

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext - 这是 UI 到处使用的上下文。它是 masterManagedObjectContext 的子级。我这样创建它:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext - 这个上下文是在我的 NSOperation 子类中创建的,它负责将 XML 数据导入核心数据。我在操作的主要方法中创建它并将其链接到那里的主上下文。

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

这实际上非常非常快。只需执行这 3 个上下文设置,我就能将导入速度提高 10 倍以上!老实说,这很难相信。(这个基本设计应该是标准核心数据模板的一部分......)

在导入过程中,我保存了 2 种不同的方式。我在后台上下文中保存的每 1000 个项目:

BOOL saveSuccess = [backgroundContext save:&error];

然后在导入过程结束时,我保存主/父上下文,表面上,它将修改推送到其他子上下文,包括主上下文​​:

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

问题:问题是我的 UI 在我重新加载视图之前不会更新。

我有一个简单的 UIViewController 和一个 UITableView,它使用 NSFetchedResultsController 提供数据。导入过程完成后,NSFetchedResultsController 看到父/主上下文没有任何变化,因此 UI 不会像我以前看到的那样自动更新。如果我将 UIViewController 从堆栈中弹出并再次加载它,所有数据都在那里。

问题:如何让我的子上下文看到父上下文中持续存在的更改,以便它们触发我的 NSFetchedResultsController 来更新 UI?

我尝试了以下只是挂起应用程序的方法:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
4

1 回答 1

47

您可能也应该大步保存主 MOC。让 MOC 等到最后保存是没有意义的。它有自己的线程,也有助于减少内存。

你写了:

然后在导入过程结束时,我保存主/父上下文,表面上,它将修改推送到其他子上下文,包括主上下文​​:

在您的配置中,您有两个孩子(主 MOC 和后台 MOC),它们都是“主”的父级。

当您保存孩子时,它会将更改推送到父母身上。该 MOC 的其他子节点将在下次执行提取时看到数据......他们没有被明确通知。

所以,当 BG 保存时,它的数据被推送到 MASTER。但是请注意,在 MASTER 保存之前,这些数据都不在磁盘上。此外,在 MASTER 保存到磁盘之前,任何新项目都不会获得永久 ID。

在您的场景中,您通过在 DidSave 通知期间从 MASTER 保存合并将数据拉入 MAIN MOC。

这应该可行,所以我很好奇它在哪里“挂”。我会注意到,您没有以规范的方式在主 MOC 线程上运行(至少对于 iOS 5 来说不是)。

此外,您可能只对合并来自主 MOC 的更改感兴趣(尽管您的注册看起来只是为了那个)。如果我要使用 update-on-did-save-notification,我会这样做......

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

现在,关于挂起的真正问题可能是什么……您显示了两个不同的调用来保存主控。第一个在它自己的 performBlock 中得到很好的保护,但第二个不是(尽管您可能在 performBlock 中调用 saveMasterContext ...

但是,我也会更改此代码...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

但是,请注意 MAIN 是 MASTER 的子项。因此,它不应该合并更改。相反,只需注意 master 上的 DidSave,然后重新获取!数据已经存在于您的父母中,等待您请求。这是首先将数据放在父级中的好处之一。

另一种需要考虑的选择(我很想听听你的结果——这是很多数据)......

与其让背景 MOC 成为 MASTER 的孩子,不如让它成为 MAIN 的孩子。

得到这个。每次 BG 保存时,它都会自动推送到 MAIN 中。现在,MAIN 必须调用 save,然后 master 必须调用 save,但所有这些都在移动指针......直到 master 保存到磁盘。

该方法的美妙之处在于数据从后台 MOC 直接进入您的应用程序 MOC(然后通过以保存)。

传递会受到一些惩罚,但是当 MASTER 击中磁盘时,所有繁重的工作都会在 MASTER 中完成。如果你用 performBlock 将这些保存踢到主线程上,那么主线程只会发送请求,并立即返回。

请告诉我进展如何!

于 2012-05-11T02:37:11.527 回答