【问题标题】:Core Data background context best practiceCore Data 后台上下文最佳实践
【发布时间】:2014-08-30 16:48:57
【问题描述】:

我有一项大型导入任务需要处理核心数据。
假设我的核心数据模型如下所示:

Car
----
identifier 
type

我从我的服务器获取汽车信息 JSON 列表,然后我想将它与我的核心数据 Car 对象同步,意思是:
如果是新车 -> 根据新信息创建一个新的核心数据 Car 对象。
如果汽车已经存在 -> 更新核心数据Car 对象。

所以我想在不阻塞 UI 的情况下在后台进行此导入,同时使用滚动显示所有汽车的汽车表视图。

目前我正在做这样的事情:

// create background context
NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

[bgContext performBlock:^{
    NSArray *newCarsInfo = [self fetchNewCarInfoFromServer]; 

    // import the new data to Core Data...
    // I'm trying to do an efficient import here,
    // with few fetches as I can, and in batches
    for (... num of batches ...) {

        // do batch import...

        // save bg context in the end of each batch
        [bgContext save:&error];
    }

    // when all import batches are over I call save on the main context

    // save
    NSError *error = nil;
    [self.mainContext save:&error];
}];

但我不确定我在这里做的是否正确,例如:

我可以使用setParentContext 吗?
我看到了一些这样使用它的示例,但是我看到了其他不调用setParentContext 的示例,而是执行了这样的操作:

NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;  
bgContext.undoManager = nil;

我不确定的另一件事是何时在主上下文中调用 save,在我的示例中,我只是在导入结束时调用 save,但我看到了使用的示例:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
}];  

正如我之前提到的,我希望用户能够在更新时与数据进行交互,那么如果我在导入更改同一辆车时用户更改了汽车类型怎么办,我编写它的方式是否安全?

更新:

感谢@TheBasicMind 的精彩解释,我正在尝试实现选项 A,所以我的代码如下所示:

这是 AppDelegate 中的 Core Data 配置:

AppDelegate.m  

#pragma mark - Core Data stack

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}  

// main
- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _managedObjectContext.parentContext = [self saveManagedObjectContext];

    return _managedObjectContext;
}

// save context, parent of main context
- (NSManagedObjectContext *)saveManagedObjectContext {
    if (_writerManagedObjectContext != nil) {
        return _writerManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _writerManagedObjectContext;
}  

这就是我的导入方法现在的样子:

- (void)import {
    NSManagedObjectContext *saveObjectContext = [AppDelegate saveManagedObjectContext];

    // create background context
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    bgContext.parentContext = saveObjectContext;

    [bgContext performBlock:^{
        NSArray *newCarsInfo = [self fetchNewCarInfoFromServer];

        // import the new data to Core Data...
        // I'm trying to do an efficient import here,
        // with few fetches as I can, and in batches
        for (... num of batches ...) {

            // do batch import...

            // save bg context in the end of each batch
            [bgContext save:&error];
        }

        // no call here for main save...
        // instead use NSManagedObjectContextDidSaveNotification to merge changes
    }];
}  

而且我还有以下观察者:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {

    NSManagedObjectContext *mainContext = self.managedObjectContext;
    NSManagedObjectContext *otherMoc = note.object;

    if (otherMoc.persistentStoreCoordinator == mainContext.persistentStoreCoordinator) {
        if (otherMoc != mainContext) {
            [mainContext performBlock:^(){
                [mainContext mergeChangesFromContextDidSaveNotification:note];
            }];
        }
    }
}];

【问题讨论】:

  • 如果您使用父子模型,您必须先保存子节点,然后再保存父节点才能正确更新数据库。
  • 这是我的问题的一部分,我应该为这种任务使用父子模型吗?我还有什么其他选择?
  • 你应该访问这篇文章你会发现它很有帮助raywenderlich.com/15916/…
  • @cabellicar123 为什么?来自核心数据编程指南:并发:Once all of the data has been consumed and turned into NSManagedObject instances, you call save on the private context, which moves all of the changes into the main queue context without blocking the main queue.developer.apple.com/library/ios/documentation/Cocoa/Conceptual/…
  • @Eyal "我正在尝试在这里进行有效的导入" 它是什么样的?

标签: ios objective-c core-data nsmanagedobjectcontext


【解决方案1】:

对于第一次接触 Core Data 的人来说,这是一个非常令人困惑的话题。我不会轻易这么说,但根据经验,我有信心说 Apple 文档在这个问题上有些误导(如果你仔细阅读它实际上是一致的,但它们并没有充分说明为什么合并数据仍然存在在许多情况下,比依赖父/子上下文并简单地从子级保存到父级更好的解决方案)。

文档给人的印象是父/子上下文是进行后台处理的新首选方式。然而,苹果忽略了一些强烈的警告。首先,请注意,您获取到子上下文的所有内容都首先通过它的父级拉取。因此,最好将在主线程上运行的主上下文的任何子上下文限制为处理(编辑)已经在主线程的 UI 中呈现的数据。如果您将它用于一般同步任务,您可能希望处理远远超出您当前在 UI 中显示的范围的数据。即使您使用 NSPrivateQueueConcurrencyType,对于子编辑上下文,您也可能会通过主上下文拖动大量数据,这可能会导致性能下降和阻塞。现在最好不要让主上下文成为您用于同步的上下文的子上下文,因为除非您要手动执行此操作,否则它不会收到同步更新通知,而且您将在您可能需要响应作为主上下文子级的编辑上下文的级联启动的保存,通过主联系人一直到数据存储。您将不得不手动合并数据,还可能跟踪需要在主上下文中无效并重新同步的内容。不是最简单的模式。

Apple 文档没有明确说明的是,您很可能需要将描述“旧”线程限制处理方式的页面上描述的技术与新的父子上下文处理方式混合在一起事物。

您最好的选择可能是(我在这里给出一个通用解决方案,最佳解决方案可能取决于您的详细要求),将 NSPrivateQueueConcurrencyType 保存上下文作为最顶层的父级,直接保存到数据存储区。 [编辑:你不会直接在这个上下文中做很多事情],然后给那个保存上下文至少两个直接的孩子。一个用于 UI 的 NSMainQueueConcurrencyType 主上下文 [编辑:最好遵守纪律,避免在此上下文上编辑任何数据],另一个是 NSPrivateQueueConcurrencyType,用于对数据进行用户编辑以及(在附图中的选项 A)您的同步任务。

然后你将主上下文作为同步上下文生成的 NSManagedObjectContextDidSave 通知的目标,并将通知 .userInfo 字典发送到主上下文的 mergeChangesFromContextDidSaveNotification:。

下一个要考虑的问题是您将用户编辑上下文(用户所做的编辑反映回界面的上下文)放置在何处。如果用户的操作总是仅限于对少量呈现数据的编辑,那么再次使用 NSPrivateQueueConcurrencyType 将其作为主上下文的子项是您最好的选择并且最容易管理(保存会将编辑直接保存到主上下文中,如果你有一个 NSFetchedResultsController,适当的委托方法将被自动调用,这样你的 UI 就可以处理更新控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath:)(这也是选项 A)。

另一方面,如果用户操作可能会导致处理大量数据,您可能需要考虑使其成为主上下文和同步上下文的另一个对等点,以便保存上下文具有三个直接子级。 mainsync(私有队列类型)和edit(私有队列类型)。我已将这种安排显示为图表上的选项 B。

与同步上下文类似,您需要在保存数据时(或者如果您需要更多粒度,在更新数据时)[编辑:配置主上下文以接收通知],并采取行动将数据合并到其中(通常使用 mergeChangesFromContextDidSaveNotification :)。请注意,通过这种安排,主上下文无需调用 save: 方法。

要了解父/子关系,请采用选项 A:父子方法只是意味着如果编辑上下文获取 NSManagedObjects,它们将首先被“复制到”(注册到)保存上下文,然后是主上下文,最后是编辑上下文。您将能够对它们进行更改,然后当您在编辑上下文中调用 save: 时,更改将仅保存到主上下文。您必须在主上下文中调用 save: ,然后在保存上下文中调用 save: ,然后才能将它们写入磁盘。

当您从子级保存到父级时,会触发各种 NSManagedObject 更改和保存通知。因此,例如,如果您使用获取结果控制器来管理 UI 的数据,则将调用它的委托方法,以便您可以适当地更新 UI。

一些后果:如果您在编辑上下文中获取对象和 NSManagedObject A,然后对其进行修改并保存,因此修改将返回到主上下文。您现在已经针对主上下文和编辑上下文注册了修改后的对象。这样做会很糟糕,但您现在可以在主上下文中再次修改对象,并且它现在与存储在编辑上下文中的对象不同。如果您随后尝试对存储在编辑上下文中的对象进行进一步修改,您的修改将与主上下文中的对象不同步,并且任何保存编辑上下文的尝试都会引发错误。

出于这个原因,使用类似于选项 A 的安排,尝试获取对象、修改它们、保存它们并重置编辑上下文(例如 [editContext reset] 与运行循环的任何一次迭代)是一个很好的模式(或在传递给 [editContext performBlock:] 的任何给定块内)。最好也遵守纪律并避免在主上下文上进行任何编辑。 此外,重新迭代,由于 main 上的所有处理都是主线程,如果您将大量对象提取到编辑上下文,主上下文将在主线程上进行提取处理 对象被迭代地从父上下文复制到子上下文。如果正在处理大量数据,这可能会导致 UI 无响应。因此,例如,如果您有大量托管对象,并且您有一个 UI 选项会导致它们都被编辑。在这种情况下,像选项 A 那样配置您的应用程序将是一个坏主意。在这种情况下,选项 B 是一个更好的选择。

如果您不处理数千个对象,那么选项 A 可能就足够了。

顺便说一句,不要太担心您选择哪个选项。如果您需要更改为 B,则从 A 开始可能是一个好主意。进行此类更改比您想象的要容易,并且通常比您预期的要少。

【讨论】:

  • 感谢您(和 +1)您的详细回答,在我开发的这一点上,添加这些额外的上下文将是一个很大的变化。我真的只想使用一个额外的上下文来执行此后台任务并使其尽可能简单。我仍然没有得到父子方法和其他替代方法之间的区别。如果您可以使用我的示例代码并修复我在那里执行的任何错误步骤,我将不胜感激,这对我来说更容易理解和执行。
  • 采用选项 A。父子方法只是意味着如果编辑上下文获取 NSManagedObjects,它们将首先被“复制到”(注册到)保存上下文,然后是主上下文,最后是编辑上下文.您将能够对它们进行更改,然后当您在编辑上下文中调用 save: 时,更改将仅保存到主上下文。您必须在主上下文中调用 save: ,然后在保存时调用 save: ,然后才能将它们写入磁盘。我会修改我的回复。
  • 我很高兴你决定在这里回答,你的信息是无价的。我想我现在更有信心尝试这个了,我只需要对我脑海中的所有这些重要信息进行排序。编辑上下文让我有点困惑,它和任何其他背景上下文有什么区别?为什么他是个孩子而同步上下文不是?无论如何,现在我认为我不会使用单独的上下文来进行用户编辑,因为用户一次只能删除一个项目(汽车)或一次更改一个属性,所以我使用主上下文进行每次编辑并直接调用保存:就可以了。
  • 我还用我编写的一些新代码更新了我的问题,试图实现选项 A(减去编辑上下文子项)。你能检查一下它是否很好,我不确定我是否需要在任何地方的 Save 上下文中调用 save:?还是合并通知中的更改就足够了?
  • @TheBasicMind 何时保存“保存”上下文?您是否独立于用户交互来执行此操作?
【解决方案2】:

首先,父/子上下文不用于后台处理。它们用于可能在多个视图控制器中创建的相关数据的原子更新。因此,如果取消最后一个视图控制器,则可以丢弃子上下文,而不会对父级产生不利影响。 Apple 在此答案的底部 [^1] 对此进行了充分解释。现在已经不碍事了,你还没有犯这个常见的错误,你可以专注于如何正确地处理后台核心数据。

创建一个新的持久存储协调器(iOS 10 不再需要,请参阅下面的更新)和一个私有队列上下文。监听保存通知并将更改合并到主上下文中(在 iOS 10 上,上下文具有自动执行此操作的属性)

有关 Apple 的示例,请参阅“地震:使用后台队列填充核心数据存储” https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html 从 2014-08-19 的修订历史中可以看出,他们添加了 “新的示例代码展示了如何使用第二个 Core Data 堆栈来获取后台队列中的数据。”

这是来自 AAPLCoreDataStackManager.m 的那一点:

// Creates a new Core Data stack and returns a managed object context associated with a private queue.
- (NSManagedObjectContext *)createPrivateQueueContext:(NSError * __autoreleasing *)error {

    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];

    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
                                                  URL:[AAPLCoreDataStackManager sharedManager].storeURL
                                              options:nil
                                                error:error]) {
        return nil;
    }

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:localCoordinator];

        // Avoid using default merge policy in multi-threading environment:
        // when we delete (and save) a record in one context,
        // and try to save edits on the same record in the other context before merging the changes,
        // an exception will be thrown because Core Data by default uses NSErrorMergePolicy.
        // Setting a reasonable mergePolicy is a good practice to avoid that kind of exception.
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        // In OS X, a context provides an undo manager by default
        // Disable it for performance benefit
        context.undoManager = nil;
    }];
    return context;
}

在 AAPLQuakesViewController.m 中

- (void)contextDidSaveNotificationHandler:(NSNotification *)notification {

    if (notification.object != self.managedObjectContext) {

        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

以下是对样本设计方式的完整说明:

地震:使用“私有”持久存储协调器在后台获取数据

大多数使用 Core Data 的应用程序都使用单个持久存储协调器来协调对给定持久存储的访问。 Earthquakes 展示了在使用从远程服务器检索的数据创建托管对象时如何使用额外的“私有”持久存储协调器。

应用架构

应用程序使用两个核心数据“堆栈”(由持久存储协调器的存在定义)。第一个是典型的“通用”堆栈;第二个由视图控制器创建,专门用于从远程服务器获取数据(从 iOS 10 开始,不再需要第二个协调器,请参阅答案底部的更新)。

主要的持久存储协调器由单例“堆栈控制器”对象(CoreDataStackManager 的一个实例)提供。它的客户有责任创建一个托管对象上下文以与协调器[^1]一起工作。堆栈控制器还提供应用程序使用的托管对象模型的属性以及持久存储的位置。客户端可以使用后面的这些属性来设置额外的持久存储协调器以与主协调器并行工作。

主视图控制器是 QuakesViewController 的一个实例,它使用堆栈控制器的持久存储协调器从持久存储中获取地震以显示在表视图中。从服务器检索数据可能是一项长时间运行的操作,需要与持久存储进行大量交互以确定从服务器检索的记录是新地震还是对现有地震的潜在更新。为了确保应用程序可以在此操作期间保持响应,视图控制器使用第二个协调器来管理与持久存储的交互。它将协调器配置为使用与堆栈控制器提供的主协调器相同的托管对象模型和持久存储。它创建一个绑定到私有队列的托管对象上下文,以从存储中获取数据并将更改提交到存储。

[^1]:这支持“传递接力棒”方法,特别是在 iOS 应用程序中,上下文从一个视图控制器传递到另一个视图控制器。根视图控制器负责创建初始上下文,并在必要时将其传递给子视图控制器。

这种模式的原因是为了确保对托管对象图的更改受到适当的约束。 Core Data 支持“嵌套”的托管对象上下文,它允许灵活的架构,从而可以轻松支持独立的、可取消的变更集。使用子上下文,您可以允许用户对托管对象进行一组更改,然后这些更改可以作为单个事务批量提交给父对象(并最终保存到商店),也可以丢弃。如果应用程序的所有部分都只是简单地从应用程序委托中检索相同的上下文,那么就会使这种行为难以或无法支持。

更新:在 iOS 10 中,Apple 将同步从 sqlite 文件级别移到了持久协调器。这意味着您现在可以创建一个私有队列上下文并重用主上下文使用的现有协调器,而不会出现以前那样的性能问题,太棒了!

【讨论】:

    【解决方案3】:

    顺便说一句,Apple 的 document 非常清楚地解释了这个问题。上面的 Swift 版本供任何感兴趣的人使用

    let jsonArray = … //JSON data to be imported into Core Data
    let moc = … //Our primary context on the main queue
    
    let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
    privateMOC.parentContext = moc
    
    privateMOC.performBlock {
        for jsonObject in jsonArray {
            let mo = … //Managed object that matches the incoming JSON structure
            //update MO with data from the dictionary
        }
        do {
            try privateMOC.save()
            moc.performBlockAndWait {
                do {
                    try moc.save()
                } catch {
                    fatalError("Failure to save context: \(error)")
                }
            }
        } catch {
            fatalError("Failure to save context: \(error)")
        }
    }
    

    如果您将NSPersistentContainer 用于 iOS 10 及更高版本,则更简单

    let jsonArray = …
    let container = self.persistentContainer
    container.performBackgroundTask() { (context) in
        for jsonObject in jsonArray {
            let mo = CarMO(context: context)
            mo.populateFromJSON(jsonObject)
        }
        do {
            try context.save()
        } catch {
            fatalError("Failure to save context: \(error)")
        }
    }
    

    【讨论】:

    • 如何获取数据你能举个例子吗,因为我在获取数据时会随机崩溃,有时会正常工作。 @hariszaman
    • 可能是您同时尝试修改和保存上下文
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多