![](/img/trans.png)
[英]Best practice manipulating Core Data Context on UI interaction while sync in background at the same time
[英]Core Data background context best practice
我需要对核心数据执行大量的导入任务。
假设我的核心数据模型如下所示:
Car
----
identifier
type
我从我的服务器获取汽车信息JSON列表,然后我想将它与我的核心数据Car
对象同步,这意味着:
如果它是一辆新车 - >从新信息创建一个新的Core Data Car
对象。
如果汽车已经存在 - >更新Core Data 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中的核心数据配置:
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];
}];
}
}
}];
对于第一次接近Core Data的人来说,这是一个非常令人困惑的话题。 我不是轻描淡写地说,但凭借经验,我有信心说Apple文档在这个问题上有些误导(如果你仔细阅读它,它实际上是一致的,但它们没有充分说明为什么合并数据仍然存在在许多情况下,比依赖父/子上下文并简单地从子节点保存到父节点更好的解决方案。
文档给出了强烈的印象,父/子上下文是进行后台处理的新首选方式。 然而,苹果忽视了一些强烈的警告。 首先,请注意,您获取到子项上下文中的所有内容都是首先通过它的父项。 因此,最好将主线程上运行的主上下文的任何子进程限制为处理(编辑)已在主线程上的UI中呈现的数据。 如果您将它用于一般同步任务,您可能希望处理的数据远远超出您当前在UI中显示的范围。 即使您使用NSPrivateQueueConcurrencyType,对于子编辑上下文,您可能会在主上下文中拖动大量数据,这可能会导致性能下降和阻塞。 现在最好不要将主上下文作为用于同步的上下文的子项,因为除非您要手动执行此操作,否则不会通知同步更新,此外,您将执行可能长时间运行的任务。您可能需要响应从作为主要上下文的子项的编辑上下文,通过主联系人向下到数据存储的级联启动的上下文。 您必须手动合并数据,还可能跟踪主要上下文中需要失效的内容并重新同步。 不是最简单的模式。
Apple文档没有说清楚的是,您最有可能需要混合使用描述“旧”线程限制处理方式的页面上描述的技术,以及新的Parent-Child上下文处理方式。
您最好的选择可能是(我在这里提供通用解决方案,最佳解决方案可能取决于您的详细要求),将NSPrivateQueueConcurrencyType保存上下文作为最顶层的父级,直接保存到数据存储区。 [编辑:你不会直接在这个上下文中做很多事情],然后给那个保存上下文至少两个直接的孩子。 一个你用于UI的NSMainQueueConcurrencyType主上下文[编辑:最好遵纪守法,避免对此上下文进行任何编辑],另一个是NSPrivateQueueConcurrencyType,用于对数据进行用户编辑,并且(在附图中的选项A)您的同步任务。
然后,将主上下文作为同步上下文生成的NSManagedObjectContextDidSave通知的目标,并将通知.userInfo字典发送到主上下文的mergeChangesFromContextDidSaveNotification:。
下一个要考虑的问题是放置用户编辑上下文的位置(用户编辑的上下文会反映回界面)。 如果用户的操作始终局限于对少量呈现数据的编辑,那么使用NSPrivateQueueConcurrencyType再次使其成为主上下文的子项是最好的选择并且最容易管理(保存将直接将编辑保存到主上下文中,如果你有一个NSFetchedResultsController,将自动调用相应的委托方法,以便你的UI可以处理更新控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath :)(同样这是选项A)。
另一方面,如果用户操作可能导致处理大量数据,您可能需要考虑将其作为主上下文和同步上下文的另一个对等方,以使保存上下文具有三个直接子级。 main , sync (私有队列类型)和编辑 (私有队列类型)。 我已将此安排显示为图表中的选项B.
与同步上下文类似,您需要[编辑:配置主上下文以接收通知]保存数据时(或者如果您需要更多粒度,更新数据时)并采取措施合并数据(通常使用mergeChangesFromContextDidSaveNotification: )。 请注意,通过这种安排,主要上下文不需要调用save:方法。
要理解父/子关系,请选择选项A:父子方法只是意味着如果编辑上下文获取NSManagedObjects,它们将首先“复制到”(注册)保存上下文,然后是主上下文,然后最后编辑上下文。 您可以对它们进行更改,然后在调用save时:在编辑上下文中,更改将仅保存到主上下文中 。 您必须在主上下文中调用save:然后在保存上下文之前调用save:才能将它们写入磁盘。
从子级保存到父级时,会触发各种NSManagedObject更改和保存通知。 因此,例如,如果您使用获取结果控制器来管理UI的数据,那么将调用它的委托方法,以便您可以根据需要更新UI。
一些结果:如果在编辑上下文中获取对象和NSManagedObject A,则修改它并保存,以便将修改返回到主上下文。 现在,您已在主要和编辑上下文中注册了已修改的对象。 这样做会很糟糕,但是你现在可以在主上下文中再次修改对象,它现在将与对象不同,因为它存储在编辑上下文中。 如果您尝试对存储在编辑上下文中的对象进行进一步修改,则您的修改将与主上下文中的对象不同步,并且任何保存编辑上下文的尝试都将引发错误。
出于这个原因,使用类似选项A的安排,尝试获取对象,修改它们,保存它们并重置编辑上下文(例如[editContext reset]是运行循环的任何单次迭代(或在任何给定的块传递给[editContext performBlock:])。最好是遵守纪律,避免对主上下文进行任何编辑。另外,重新迭代,因为main上的所有处理都是主线程,如果你获取对于编辑上下文有很多对象,主要上下文将在主线程上进行获取处理,因为这些对象正在从父到子上下文中被迭代复制。如果有大量数据被处理,这可能会导致无响应例如,如果你有一大堆托管对象,并且你有一个UI选项会导致它们都被编辑。在这种情况下配置你的应用程序就像选项A一样。这种情况选项B是更好的选择。
如果您没有处理数千个对象,那么选项A可能就足够了。
BTW对你选择哪个选项不要太担心。 从A开始并且如果您需要更改为B可能是一个好主意。这比您想象的更容易做出这样的改变,并且通常比您预期的后果更少。
首先,父/子上下文不用于后台处理。 它们用于可能在多个视图控制器中创建的相关数据的原子更新。 因此,如果取消了最后一个视图控制器,则可以丢弃子上下文,而不会对父项产生负面影响。 苹果在[^ 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的应用程序都使用单个持久性存储协调器来调解对给定持久性存储的访问。 地震显示了在使用从远程服务器检索的数据创建托管对象时如何使用其他“私有”持久性存储协调器。
应用架构
该应用程序使用两个核心数据“堆栈”(由持久性存储协调器的存在定义)。 第一个是典型的“通用”堆栈; 第二个是由视图控制器创建的,专门用于从远程服务器获取数据(从iOS 10开始,不再需要第二个协调器,请参阅答案底部的更新)。
主持久性存储协调器由单个“堆栈控制器”对象(CoreDataStackManager的实例)提供。 客户有责任创建托管对象上下文以与协调器[^ 1]一起使用。 堆栈控制器还会检查应用程序使用的托管对象模型的属性以及持久性存储的位置。 客户端可以使用后面的这些属性来设置其他持久性存储协调器,以与主协调器并行工作。
主视图控制器是QuakesViewController的一个实例,它使用堆栈控制器的持久存储协调器从持久存储中获取地震以显示在表视图中。 从服务器检索数据可以是长期运行的操作,其需要与持久存储进行大量交互以确定从服务器检索的记录是新地震还是对现有地震的潜在更新。 为确保应用程序在此操作期间能够保持响应,视图控制器使用第二个协调器来管理与持久性存储的交互。 它将协调器配置为使用相同的托管对象模型和持久存储作为堆栈控制器提供的主协调器。 它创建绑定到专用队列的托管对象上下文,以从存储中获取数据并将更改提交到存储。
[^ 1]:这支持“传递接力棒”方法,特别是在iOS应用程序中,上下文从一个视图控制器传递到另一个视图控制器。 根视图控制器负责创建初始上下文,并在必要时将其传递给子视图控制器。
此模式的原因是确保对托管对象图的更改进行适当约束。 Core Data支持“嵌套”托管对象上下文,允许灵活的体系结构,以便轻松支持独立,可取消的更改集。 使用子上下文,您可以允许用户对托管对象进行一组更改,然后可以将这些更改作为单个事务批量提交给父级(最终保存到存储),或者丢弃。 如果应用程序的所有部分只是从应用程序委托中检索相同的上下文,则会使此行为难以或无法支持。
更新:在iOS 10中,Apple将同步从sqlite文件级别移至持久协调器。 这意味着您现在可以创建一个私有队列上下文并重用主上下文使用的现有协调器,而不会出现以前那样的性能问题,很酷!
顺便说一句,Apple的这份文件非常清楚地解释了这个问题。 上面的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)")
}
}
如果您使用适用于iOS 10及更高版本的NSPersistentContainer ,甚至更简单
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)")
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.