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

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

这是设置:

您有一个应用程序,它可以下载并添加大量 XML 数据(大约200万条记录,每条记录的大小大致相当于一段正常文本的大小)。Sqlite 文件大小约为500MB。将这些内容添加到 Core Data 需要时间,但是您希望用户能够在数据以递增方式加载到数据存储中时使用这个应用程序。大量的数据被移动,这对用户来说是不可见和不可察觉的,所以没有挂起,没有紧张: 滚动就像黄油一样。尽管如此,这个应用程序更有用,添加的数据越多,所以我们不能永远等待数据被添加到核心数据存储。在代码中,这意味着我真的希望在导入代码中避免这样的代码:

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

这个应用程序只支持 iOS5,所以它需要支持的最慢的设备是 iPhone3GS。

以下是我到目前为止用来开发我当前解决方案的资源:

苹果核心数据编程指南: 高效导入数据

  • 使用自动释放池来降低内存
  • 人际关系的代价。进口单位,然后在最后修补关系
  • 不要询问你是否能够帮助它,它会以 O (n ^ 2)的方式减慢速度
  • 批量导入: 保存,重置,排水和重复
  • 在导入时关闭撤消管理器

核心数据性能

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

IDeveloper TV-Mac、 iPhone 和 iPad 的核心数据更新

  • 使用 PerformBlock 运行对其他队列的保存可以加快速度。
  • 加密会减慢速度,如果可以就关掉。

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

  • 通过给当前运行循环留出时间,可以减慢导入的速度, 这样使用者会觉得很光滑。
  • 示例代码证明,可以进行大量导入并保持 UI 响应,但是速度不如3个上下文和异步保存到磁盘的速度。

我目前的解决方案

我有3个 NSManagedObjectContext 实例:

Master ManagedObjectContext -这个上下文具有 NSPersisentStore 协调器,负责将文件保存到磁盘。我这样做是为了我的保存可以是异步的,因此非常快。我在发射时创建它,像这样:

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

MainManagedObjectContext -这是 UI 到处使用的上下文。它是 master ManagedObjectContext 的子代。我是这样创造的:

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

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

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,它使用 NSFetcheResultsController 提供数据。当 Import 过程完成时,NSFetchResultsController 不会看到来自父/主上下文的更改,因此 UI 不会像我习惯看到的那样自动更新。如果我从堆栈中弹出 UIViewController 并再次加载它,所有的数据都在那里。

问题 : 如何让我的子上下文看到在父上下文上持久存在的更改,以便它们触发我的 NSFetpedResultsController 来更新 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];
}
13118 次浏览

You should probably save the master MOC in strides as well. No sense having that MOC wait until the end to save. It has its own thread, and it will help keep memory down as well.

You wrote:

Then at the end of the import process, I save on the master/parent context which, ostensibly, pushes modifications out to the other child contexts including the main context:

In your configuration, you have two children (the main MOC and the background MOC), both parented to the "master."

When you save on a child, it pushes the changes up into the parent. Other children of that MOC will see the data the next time they perform a fetch... they are not explicitly notified.

So, when BG saves, its data is pushed to MASTER. Note, however, that none of this data is on disk until MASTER saves. Furthermore, any new items will not get permanent IDs until the MASTER saves to disk.

In your scenario, you are pulling the data into the MAIN MOC by merging from the MASTER save during the DidSave notification.

That should work, so I'm curious as to where it is "hung." I will note, that you are not running on the main MOC thread in the canonical way (at least not for iOS 5).

Also, you probably only are interested in merging changes from the master MOC (though your registration looks like it is only for that anyway). If I were to use the update-on-did-save-notification, I'd do this...

- (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
}];
}

Now, for what may be your real issue regarding the hang... you show two different calls to save on the master. the first is well protected in its own performBlock, but the second is not (though you may be calling saveMasterContext in a performBlock...

However, I'd also change this code...

- (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];
}];
}

However, note that the MAIN is a child of MASTER. So, it should not have to merge the changes. Instead, just watch for the DidSave on the master, and just refetch! The data is sitting in your parent already, just waiting for you to ask for it. That's one of the benefits of having the data in the parent in the first place.

Another alternative to consider (and I'd be interested to hear about your results -- that's a lot of data)...

Instead of making the background MOC a child of the MASTER, make it a child of the MAIN.

Get this. Every time the BG saves, it automatically gets pushed into the MAIN. Now, the MAIN has to call save, and then the master has to call save, but all those are doing is moving pointers... until the master saves to disk.

The beauty of that method is that the data goes from the background MOC straight into your applications MOC (then passes through to get saved).

There is some penalty for the pass-through, but all the heavy lifting gets done in the MASTER when it hits the disk. And if you kick those saves on the master with performBlock, then main thread just sends off the request, and returns immediately.

Please let me know how it goes!