前言:本文是我几个月前的这篇 其中的一个问题的回答,这几天整理博客,更新内容,自觉有能力回答这个问题了。这篇单独拿出来首先是因为这个问题很不错,值得单独写一篇;其次为了便于检索,因为简书目前不支持标签,只能通过文集来分类,有点不方便,折腾个优美的基于 Github 的博客又嫌麻烦,暂时还是在这里写吧,等有精力了迁移;最后是因为这个回答写得太长了,原本打算写个大纲型的,但由于回答是定位于基础,所以加了很多基础知识的介绍和补充,这样一来,原文就更长了。我这里的很多文章都写得太长,罗里吧嗦的,我已经难以忍受,但目前我还没太多能力进行精简。
老实说,当时写的东西大部分只是搬运而已,是我的博客里很水的一篇,但却是我这么多文章里最受欢迎的一篇。能说明什么,大家都很水呗,水不要紧,日拱一卒,共勉。第一步:搭建 Core Data 多线程环境
这个问题首先要解决的是搭建 Core Data 多线程环境。Core Data 对并发模式的支持非常完备,NSManagedObjectContext 的指定初始化方法中就指定了并发模式:init(concurrencyType ct: NSManagedObjectContextConcurrencyType)
有三种模式:
从 iOS 9 开始就剩下后面两种模式了,那么搭建多线程 Core Data 环境的方案一般如下,创建一个 NSMainQueueConcurrencyType
的 context 用于响应 UI 事件,其他涉及大量数据操作可能会阻塞 UI 的,就使用 NSPrivateQueueConcurrencyType
的 context。
let mainContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)let backgroundContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
但需要注意的是「Core Data 使用线程或者序列化的队列来保护 managed objects 和 managed object context,因此 context 假设它的默认拥有者是它初始化时分配的线程或队列,你不能在某个线程中初始化一个 context 后传递给另外一个线程来使用它。」这段蹩脚的话是我从NSManagedObjectContext
的文档翻译来的,意思就是说 managed object context 并非线程安全的,你不能随便地开启一个后台线程访问 managed object context 进行数据操作就管这叫支持多线程了,那么应该怎么做呢?官方文档为我们做了示范,在 private queue 的 context 中进行操作时,应该使用以下方法:
func performBlock(_ block: () -> Void)//在私有队列中异步地执行 Blcok func performBlockAndWait(_ block: () -> Void)//在私有队列中执行 Block 直至操作结束才返回
要在不同线程中使用 managed object context 时,不需要我们创建后台线程然后访问 managed object context 进行操作,而是交给 context 自身绑定的私有队列去处理,我们只需要在上述两个方法的 Block 中执行操作即可。事实上,你也可以在其他线程中来使用 context,但是要保证以上两个方法。而且,在 NSMainQueueConcurrencyType
的 context 中也应该使用这种方法执行操作,这样可以确保 context 本身在主线程中进行操作。
题外话,在构建多线程 context 时,经常会出现这样的局面:Multi-contexts vs Concurrency。前者可能有更加复杂的情况,在 iOS 5之后,context 可以指定父 context,persistent store coordinator 不再是其与 persistent store 联系的唯一选择。需要注意的是,子 context 的 fetch 和 save 操作都会交给父 context 来完成,对于子 context 的 save 操作,只会到达上一层的父 context 里,只有父 context 执行了 save 操作,子 context 中的变化才会提交到 persistent store 保存。这种子 context 适合在后台执行长时间操作,比如在后台里在子 context 里导入大量数据,在主线程的父 context 里更新进度。另外一种是平行的多 context。Concurrency 的特性也是在 iOS 5 后开始支持,这个特性减少了平行的多 context 的需求。关于这个话题,可以看这篇文章:。
第二步:数据的同步操作
总的来说,在多 context 环境的下,context 的生命周期里有两个阶段需要处理数据同步的问题。当某个 context 里的状态发生了变化并执行保存来更新 persistent store后,对于其他 context 来说有两个选择:1. persistent store 更新后,此时其他 context 与 persistent store 进行同步;2. persistent store 更新后,其他 context 并不立即同步,而在自身进行保存时与 persistent store 进行同步,两者有差异时需要解决冲突问题。前者采取的是「一处修改处处同步」的策略,所有的 context 中同步为一个版本,后者采取的是「多版本共存协商处理」的策略。以下讨论都基于多个 context,假设应用配置了两个 managed object context,mainContext,在主线程运行 ,另外一个 backgroundContext 用于后台处理。多 context 单数据版本
在 context 中执行保存时,应用并不会主动告知其他 context。那么如何在多个 context 间进行通信呢?Core Data 提供了通知机制,context 执行保存时,会发出以下通知:
1.NSManagedObjectContextWillSaveNotificationManaged object context 即将执行保存操作时发出此通知,没有附带任何信息。2.NSManagedObjectContextDidSaveNotificationManaged object context 的保存操作完成之后由该 context 自动发出此通知,包含了所有的新增、更新和删除的对象的信息。注意在通知里的 managed objects 只能在 context 所在的线程里使用。由于 context 也只能在自身的线程里执行操作,所以没法直接使用通知里的 managed objects,这时候应该通过 managed object 的 objectID 以及 context 的objectWithID(_ objectID: )
来获取。 合并其他 context 的数据可以通过mergeChangesFromContextDidSaveNotification(_ notification:)
来完成。在这个方法里,context 会更新发出通知的 context 里变化的任何同样的对象,引入新增的对象并处于 faults 状态,并删除在发出通知的 context 里已经删除的对象。
代码示例,在 backgroundContext 中编辑后使 mainContext 与之同步:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "backgroundContextDidSave:", name: NSManagedObjectContextDidSaveNotification, object: backgroundContext) func backgroundContextDidSave(notification: NSNotification){ mainContext.performBlock(){ mainContext.mergeChangesFromContextDidSaveNotification(notification) } }
多 context 多数据版本
在这种方案下,backgroundContext 并不针对 mainContext 的保存做出反应。在 mainContext 和 backgroundContext 中 fetch 了同类的 managed objects,两个 context 都发生了变化并且变化不一样,此时让 backgroundContext 与 mainContext 前后脚分别执行保存的话,就会发生冲突导致后者保存失败。
在 managed object context 中执行 fetch 操作时,会对 persistent store 里的状态进行快照,当 context 执行保存时,会使用快照与 persistent store 进行对比,如果状态不一致,说明 persistent store 在其他地方被更改了,而这个变化并不是当前 context 造成的,这样就造成了当前 context 状态的不连续,此时保存就会产生冲突。这里需要介绍 managed object context 的属性mergePolicy
,这个属性指定了 context 的合并策略,决定了保存时合并数据发生冲突时如何应对,该属性有以下几种值:
除了默认的 NSErrorMergePolicy
在发生冲突时返回错误等待下一步处理外,其他的合并策略直接根据自身的规则来处理合并冲突,因此在选择时要谨慎处理。从上面的解释来看,似乎NSMergeByPropertyStoreTrumpMergePolicy
与NSRollbackMergePolicy
没什么区别,NSMergeByPropertyObjectTrumpMergePolicy
与 NSOverwriteMergePolicy
也没有什么区别。区别在于怎么对待被覆盖的一方中没有冲突的变化,NSMergeByPropertyStoreTrumpMergePolicy
和NSMergeByPropertyObjectTrumpMergePolicy
采取的是局部替换,前者 context 中没有冲突的变化不会受到影响,后者 persistent store 中没有冲突的变化不受影响;NSOverwriteMergePolicy
和 NSRollbackMergePolicy
采取的是全局替换,persistent store 和 context 中只有一方的状态得以保留。
回到本节开始的场景,mainContext 和 backgroundContext 中的版本不一致,会产生合并冲突,解决方案有以下两种选择:
1.不管 mainContext 中是否发生改变,与 backgroundContext 中状态同步;//此时 mainContext 和 backgroundContext 都采用默认合并策略即可。mainContext.performBlock(){ do{ try mainContext.save() } catch { /* 清空 mainContext,其中所有的 managed objects 消失。 如果引用了其中的 managed objects 的话,注意在 reset 前取消对这些对象的引用。 */ mainContext.reset() //重新 fetch let fetchRequest = ... let updatedFetchedResults = try mainContext.executeFetchRequest(fetchRequest) } }
又或者,mainContext 的合并策略采用NSMergeByPropertyStoreTrumpMergePolicy
或NSRollbackMergePolicy
,这样就省去了 reset 操作。实际上,采用这种方案不如上一个策略来得方便。
NSOverwriteMergePolicyType
或NSMergeByPropertyObjectTrumpMergePolicy
,而且执行保存时不会返回错误,不需要后续的处理。 小结
同步多个 context 是个比较复杂的事情,需要根据具体的需要来设定 context 的合并策略以及选择同步的时机,不仅仅限于以上的两种策略,融合两种策略也可以,当然那样可能会大大增加复杂度,更容易导致 Bug。另外,还有一种使用 child context 的方法,就是将其他 context 作为 context 的 parentContext,这种方法没有研究,自己有兴趣可以试试。
1.同步问题第一原则:不要跨线程使用 managed object,而应该通过其对应的 objectID,在其他线程里的 context 里来获取对象。2.NSManagedObjectContext
的合并方法mergeChangesFromContextDidSaveNotification(_ notification:)
可以替完全复制另一个 context 的状态;如果你不想完全复制,可以使用更精确的方法refreshAllObjects()
,这是 iOS 9 中推出的新方法;或者手动处理,当然,不推荐这么做。3.利用NSMergePolicy
来处理同步相对而言危险一点,你得明确知道你在做什么。 最后一站:大量数据操作
从上面的内容可以得知,在多线程环境下同步数据基本上不需要我们手动去处理 managed objects 的同步,因此处理大量数据的同步,关注的重点更多在于内存占用和性能。写代码要记住以下几点:
1.涉及大量数据的操作尽量要放在后台线程里处理,防止阻塞主线程;对于多 context 的结构,可以参考这篇文章,作者通过验证,证明了「设置一个 persistent store coordinator 和两个独立的 contexts 被证明了是在后台处理 Core Data 的好方法」。2.能够保持 faults 状态的 managed objects 尽量不要触发 fire,降低内存占用,同时也能提升响应速度。3.fetch 大量数据时注意技巧,可以通过利用 predicate 来筛选实际需要的数据,限制 fetch 的总数量,设定合适的批量获取数量来降低 IO 的频次,这些需要在实际环境中寻找平衡点。4.尽量让 context 中的 entity 类别少一些,降低对同步的需求。(从 iOS 8 开始,Core Data 在性能方面有了较大的提升,尽量合理利用。)5.使用异步请求 Asynchronous Fetch,尽管可以将 fetch 大量数据的操作放在后台线程里,但是这样依然会阻塞那个线程,使用异步请求,则依然可以在后台线程里进行其他操作,并且还有方便的进度提示和取消功能。6.使用批量更新 Batch Update,有效降低内存占用并大幅提升保存的速度。以往在NSManagedObjectContext
中进行保存时,只能将其中包含的变化进行保存,而 Batch Update 则是直接对 persistent store 进行更新而不需要将 managed objects 读入内存,可以大幅降低内存占用而且更新速度提升不少。但需要注意的是,使用批量更新并不会提醒 context,需要我们对 context 手动进行更新,而且没有进行有效验证,也需要开发者来保证有效性。7.使用批量删除 Batch Delete,与批量更新类似,直接对 persistent store 进行操作,效率非常高,也有着和批量更新类似的问题。8.使用 iOS 9 新增的NSManagedObjectContext
的新 API:refreshAllObjects()
,该方法会对 context 中注册的所有对象进行刷新,还没有保存的变化也会得到保留,这样就可以解放 6 和 7 中的手动更新工作。 写了 103464 字,被 2170 人关注,获得了 962 个喜欢
有一个疑问,比如一个backgroundContext 删除了几条数据 ,而对应的mainContext对这几天数据进行了操作;如果对mainContext进行了save,更新了到了PSC(persistent store),此时对backgroundContext进行了refreshAllObjects(),那删除的数据还在不?应对这个conflict, mergeChangesFromContextDidSaveNotification和refreshAllObjects(),有差别吗?还是说,就是完全还原了数据,然后再次进行数据操作...还有
if #available(iOS 9, *) { //只影响 mainContext 中注册的 managed objects,但不引入 backgroundContext 新添加的对象。 mainContext.refreshAllObjects() }这段代码,如果说refreshAllObjects完全还原了数据,后面没了数据操作,还是没有保存成功,这还有意义吗: refreshAllObjects() 会保留尚未保存的变化,那么mainContext 里删除了东西保存后,在 backgroundContext 里 refreshAllObjects() 后还会留下那些被删除的对象,但 PSC里面已经删除了,所以这里我犯了错,不应该这样处理。我修改下文章。
博主,您好,这段时间在研究CoreData,关于多线程我看到了 设置层级上下文的策略,还有今天看到您的文章,多上下文同步数据,我非常的不明白,我亲自试验了一下,直接使用一个NSPrivateQueueConcurrencyType的context。调用performBolck方法,执行,赠,删,改,查,完全是可行的,然后调用dispath mianqueue,刷新界面,刷新数据,完全是可以的啊,为什么要使用NSMainQueueConcurrencyType的context呢,还要同步2个上下文的数据,还请博主解答,多谢多谢
_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [_backgroundContext performBlockAndWait:^{ [_backgroundContext setPersistentStoreCoordinator:_coordinator]; [_backgroundContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy]; }];//获得数据[self.cdh.backgroundContext performBlock:^{ NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"]; NSArray *ary = [self.cdh.backgroundContext executeFetchRequest:request error:nil]; [weakSelf.array addObjectsFromArray:ary]; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.tableView reloadData]; }); }];//修改数据- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ [tableView deselectRowAtIndexPath:indexPath animated:YES]; Person *p = self.array[indexPath.row]; p.nName = @"saaa"; [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; NSManagedObjectContext *context = self.cdh.backgroundContext; [context performBlock:^{ [context save:nil]; }];}: 你在这个场景很简单的例子里这么做没有问题,其实这个例子里就不需要多线程 context,还有同步的麻烦。多线程 context 的例子可以看这里:
: 感谢博主的回复,我大概看了一下代码,作者好像使用了NSFetchedResultsController,这个是内嵌的一些通知来更新tableview,而且NSFetchedResultsController好像只能接受 mainqueue形式的context,额。通过合并数据到主context,我觉得NSFetchedResultsController还是会在主线程执行一次查询操作,然后刷新表格,其实在拉动的时候还是很卡,不过已经很好了吧,我还是有点不理解,
NSArray *ary = [self.cdh.backgroundContext executeFetchRequest:request error:nil];[weakSelf.array addObjectsFromArray:ary];dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf.tableView reloadData];});我这句可能跨线程访问了把- ,-: 主线程只有一个,所以你在 backgroundContext 的私有队列里切换到主队列里刷新 UI 是没有问题的。你说『NSFetchedResultsController好像只能接受 mainqueue形式的context』,刷新 UI 只能在主线程里进行,如果 NSFetchedResultsController 发现绑定的 context 发生变化而刷新 UI 时不在主线程,必定有延迟和卡顿,你大概就是你说的好像只能接受mainqueue形式的context。