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

Posted

技术标签:

【中文标题】在 iOS 5 上实现快速高效的核心数据导入【英文标题】:Implementing Fast and Efficient Core Data Import on iOS 5 【发布时间】:2012-05-10 21:03:06 【问题描述】:

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

设置如下:

您有一个应用程序,它下载并添加大量 XML 数据(大约 200 万条记录,每条记录大约是普通文本段落的大小)。.sqlite 文件的大小约为 500 MB。将此内容添加到 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 个上下文:主上下文类型、主要上下文类型和限制上下文类型

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 实例:

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

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

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

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

backgroundContext - 这个上下文是在我的 NSOperation 子类中创建的,它负责将 XML 数据导入核心数据。我在操作的 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,它使用 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];

【问题讨论】:

+1000000 是有史以来格式最好、准备最充分的问题。我也有答案...不过需要几分钟才能输入... 当你说应用挂起时,它在哪里?它在做什么? 抱歉很久才提出这个问题。您能否澄清一下“导入平面,最后修补关系”是什么意思?为了建立关系,您是否还必须在内存中拥有这些对象?我正在尝试实施一个与您的解决方案非常相似的解决方案,我真的可以使用一些帮助来降低内存占用。 查看链接到本文第一篇的 Apple Docs。它解释了这一点。祝你好运! 非常好的问题,我从您提供的设置描述中获得了一些巧妙的技巧 【参考方案1】:

您可能也应该大步拯救主 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 将这些保存踢到 master 上,那么主线程只会发送请求,并立即返回。

请告诉我进展如何!

【讨论】:

优秀的答案。我今天将尝试这些想法,看看我发现了什么。谢谢! 太棒了!效果很好!不过,我将尝试您对 MASTER -> MAIN -> BG 的建议,看看效果如何,这似乎是一个非常有趣的想法。谢谢你的好主意! 更新为将 performBlockAndWait 更改为 performBlock。不知道为什么这会再次出现在我的队列中,但是当我这次阅读它时,很明显......不知道为什么我之前放过它。是的, performBlockAndWait 是可重入的。但是,在这样的嵌套环境中,您不能从父上下文中调用子上下文的同步版本。通知可以(在这种情况下)从父上下文发送,这可能导致死锁。我希望以后来读这篇文章的人都清楚这一点。谢谢,大卫。 @DavidWeiss 你试过 MASTER -> MAIN -> BG 吗?我对这种设计模式很感兴趣,希望知道它是否适合你。谢谢。 MASTER -> MAIN -> BG 模式的问题是当你从 BG 上下文中获取时,它也会从 MAIN 中获取,这会阻塞 UI 并使你的应用程序没有响应

以上是关于在 iOS 5 上实现快速高效的核心数据导入的主要内容,如果未能解决你的问题,请参考以下文章

Mysql索引

MySql索引学习总结

一文快速入门 MySQL 索引

在 iOS 5 中使用 UIManagedDocument 和父/子上下文导入核心数据背景

将核心数据添加到现有的选项卡式应用程序(ios swift、Xcode6)

在PaddlePaddle上实现MNIST手写体数字识别