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


101

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

设置如下:

您有一个应用程序可以下载并添加大量XML数据(大约200万条记录,每个记录的大小均与正常文本的大小相同)。.sqlite文件的大小约为500 MB。将此内容添加到Core Data需要花费时间,但是您希望用户能够在数据逐步加载到数据存储中时使用该应用程序。用户必须看不见并且无法感知大量数据在四处移动,因此不会挂起,也不会抖动:滚动像黄油一样。尽管如此,该应用程序仍然有用,它添加了更多数据,因此我们不能永远等待将数据添加到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导入和显示核心数据中的大数据集

  • 您可以通过给当前的运行循环一些时间来减慢导入速度,从而使用户感觉平稳。
  • 示例代码证明,可以进行大量导入并保持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方法中创建它,并将其链接到那里的master上下文。

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才会更新。

我有一个带有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];
}

26
+1000000是有史以来形成最好,准备最充分的问题。我也有一个答案...虽然需要几分钟才能输入...
乔迪·哈金斯

1
当您说应用程序挂起时,它在哪里?到底在做什么
乔迪·哈金斯

很抱歉,经过很长时间才提出来。您能否说明“导入平面,然后在最后修补关系”是什么意思?您是否还必须在内存中保留这些对象才能建立关系?我正在尝试实现与您的解决方案非常相似的解决方案,并且我真的可以使用一些帮助来减少内存占用。
安德里亚·斯普雷加

请参阅链接到本文第一部分的Apple文档。它解释了这一点。祝好运!
David Weiss

1
真的是一个很好的问题,我从您提供的设置说明中吸取了一些巧妙的技巧
djskinner 2013年

Answers:


47

您也应该大步保存主MOC。那个MOC一直等到保存结束没有意义。它有自己的线程,也将有助于减少内存。

你写了:

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

在您的配置中,您有两个子代(主MOC和后台MOC),均作为“主”母代。

当您保存孩子时,它将更改推入父级。该MOC的其他子项在下次执行提取操作时将看到数据...未明确通知它们。

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

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

那应该起作用,所以我很好奇它在哪里“挂”了。我将注意到,您不是以规范的方式在MOC主线程上运行(至少不是针对iOS 5)。

另外,您可能只对合并来自主MOC的更改感兴趣(尽管您的注册看起来只不过是用于此目的)。如果我要使用“更新时保存通知”,请执行此操作...

- (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必须调用保存,然后主机必须调用保存,但是所有这些操作都是在移动指针...直到主机保存到磁盘。

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

传递会受到一些损失,但是当MASTER碰到磁盘时,所有繁重的工作都会在MASTER中完成。并且,如果您使用performBlock将这些节省踢到master上,那么主线程只会发出请求,并立即返回。

请让我知道情况如何!


极好的答案。我今天将尝试这些想法,然后看看我发现了什么。谢谢!
大卫·魏斯

太棒了!效果很好!不过,我将尝试您对MASTER-> MAIN-> BG的建议,看看这种效果如何发挥出来,这似乎是一个非常有趣的想法。谢谢你的好主意!
大卫·魏斯

4
更新以将performBlockAndWait更改为performBlock。不知道为什么它再次出现在我的队列中,但是当我这次读它时,很明显……不知道为什么我以前放过它。是的,performBlockAndWait是可重入的。但是,在这样的嵌套环境中,您不能在父上下文中的子上下文上调用同步版本。可以(在这种情况下)从父上下文发送通知,这可能导致死锁。我希望所有后来阅读此书的人都清楚。谢谢大卫。
乔迪·哈金斯

1
@DavidWeiss您是否尝试过MASTER-> MAIN-> BG?我对此设计模式感兴趣,希望知道它是否对您有效。谢谢。
消极的2012年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.