在iOS 5上实现快速高效的核心数据导入

时间:2012-05-10 21:03:06

标签: ios core-data nsfetchedresultscontroller nsmanagedobjectcontext

问题:如何获取子上下文以查看父上下文中保留的更改,以便它们触发我的NSFetchedResultsController来更新UI?

以下是设置:

你有一个下载并添加大量XML数据的应用程序(大约200万条记录,每条记录大致与正常文本段的大小相同).sqlite文件的大小约为500 MB。将此内容添加到Core Data需要时间,但您希望用户能够在数据以递增方式加载到数据存储中时使用该应用程序。用户必须看不到大量数据被移动,因此没有挂起,没有抖动:滚动像黄油一样。尽管如此,该应用程序更有用,添加的数据越多,因此我们不能永远等待将数据添加到Core Data存储中。在代码中,这意味着我真的想在导入代码中避免使用这样的代码:

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

该应用仅限iOS 5,因此需要支持的最慢设备是iPhone 3GS。

以下是我目前用于开发当前解决方案的资源:

Apple's Core Data Programming Guide: Efficiently Importing Data

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

iDeveloper TV - Core Data Performance

  • 使用3种上下文:Master,Main和Confinement上下文类型

iDeveloper TV - Core Data for Mac, iPhone & iPad Update

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

Importing and Displaying Large Data Sets in Core Data by Marcus Zarra

  • 您可以通过为当前运行循环提供时间来减慢导入速度, 所以事情让用户感觉很顺利。
  • 示例代码证明可以执行大量导入并保持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数据导入Core Data。我在操作的main方法中创建它并将其链接到那里的主上下文。

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

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

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

BOOL saveSuccess = [backgroundContext save:&error];

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

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

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

我有一个带有UITableView的简单UIViewController,它使用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];
}

1 个答案:

答案 0 :(得分:47)

你也应该大步保存主MOC。没有意义让MOC等到结束才能保存。它有自己的线程,它也有助于保持内存。

您写道:

  

然后在导入过程结束时,我保存在master / parent上   表面上看,将修改推向另一个孩子的语境   上下文包括主要背景:

在您的配置中,您有两个孩子(主要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的孩子。因此,它不应该合并更改。相反,只需在主人身上观看DidSave,然后重新获取!数据已经存在于您的父母中,只是等待您的要求。这是在父母中首先获得数据的好处之一。

另一个需要考虑的选择(我很想知道你的结果 - 这是很多数据)......

不要让背景MOC成为MASTER的孩子,而是让它成为MAIN的孩子。

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

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

传递通过一些惩罚,但是当它击中磁盘时,所有繁重的工作都会在MASTER中完成。如果你使用performBlock在主服务器上执行这些保存,那么主线程就会发送请求,并立即返回。

请告诉我它是怎么回事!