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

Dav*_*iss 98 core-data nsfetchedresultscontroller nsmanagedobjectcontext ios

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

这是设置:

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

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

iDeveloper TV - 核心数据性能

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

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

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

Marcus Zarra在核心数据中导入和显示大型数据集

  • 您可以通过为当前运行循环提供时间来减慢导入速度,因此用户感觉很顺利.
  • 示例代码证明可以执行大型导入并保持UI响应,但不能像使用3个上下文和异步保存到磁盘一样快.

我目前的解决方案

我有3个NSManagedObjectContext实例:

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

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
Run Code Online (Sandbox Code Playgroud)

mainManagedObjectContext - 这是UI在任何地方使用的上下文.它是masterManagedObjectContext的子代.我像这样创建它:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];
Run Code Online (Sandbox Code Playgroud)

backgroundContext - 此上下文在我的NSOperation子类中创建,该子类负责将XML数据导入Core Data.我在操作的main方法中创建它并将其链接到那里的主上下文.

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];
Run Code Online (Sandbox Code Playgroud)

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

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

BOOL saveSuccess = [backgroundContext save:&error];
Run Code Online (Sandbox Code Playgroud)

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

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];
Run Code Online (Sandbox Code Playgroud)

问题:问题是我的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];
}
Run Code Online (Sandbox Code Playgroud)

Jod*_*ins 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
    }];
}
Run Code Online (Sandbox Code Playgroud)

现在,关于挂起的真正问题......你会显示两个不同的电话来保存主人.第一个在自己的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];
    }];
}
Run Code Online (Sandbox Code Playgroud)

但请注意,MAIN是MASTER的孩子.因此,它不应该合并更改.相反,只需在主人身上观看DidSave,然后重新获取!数据已经存在于您的父母中,只是等待您的要求.这是在父母中首先获得数据的好处之一.

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

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

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

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

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

请让我知道它是怎么回事!

  • 更新以将performBlockAndWait更改为performBlock.不知道为什么这会在我的队列中再次出现,但是当我这次读它时,很明显......不知道为什么我之前放弃它.是的,performBlockAndWait是可重入的.但是,在这样的嵌套环境中,您无法在父上下文中调用子上下文中的同步版本.通知可以(在这种情况下)从父上下文发送,这可能导致死锁.我希望任何过来的人都能清楚地看到这一点.谢谢,大卫. (4认同)
  • MASTER - > MAIN - > BG模式的问题是当您从BG上下文获取时,它也会从MAIN获取并且会阻止UI并使您的应用程序无法响应 (2认同)