http://www.jianshu.com/p/37ab8f336f76javascript
前言:本文是我几个月前的这篇《iOS 面试基础题目》 其中的一个问题的回答,这几天整理博客,更新内容,自觉有能力回答这个问题了。这篇单独拿出来首先是由于这个问题很不错,值得单独写一篇;其次为了便于检索,由于简书目前不支持标签,只能经过文集来分类,有点不方便,折腾个优美的基于 Github 的博客又嫌麻烦,暂时仍是在这里写吧,等有精力了迁移;最后是由于这个回答写得太长了,本来打算写个大纲型的,但因为回答是定位于基础,因此加了不少基础知识的介绍和补充,这样一来,原文就更长了。我这里的不少文章都写得太长,罗里吧嗦的,我已经难以忍受,但目前我还没太多能力进行精简。
老实说,当时写的东西大部分只是搬运而已,是个人博客里很水的一篇,但倒是我这么多文章里最受欢迎的一篇。能说明什么,你们都很水呗,水没关系,日拱一卒,共勉。html
第一步:搭建 Core Data 多线程环境
这个问题首先要解决的是搭建 Core Data 多线程环境。Core Data 对并发模式的支持很是完备,NSManagedObjectContext 的指定初始化方法中就指定了并发模式:java
init(concurrencyType ct: NSManagedObjectContextConcurrencyType)
有三种模式:1.NSConfinementConcurrencyType
这种模式是用于向后兼容的,使用这种模式时你应该保证只能在建立的线程里使用 context,然而这不容易获得保证。关于此模式的最新消息是 iOS 9 中它将被废弃,不推荐使用。
2.NSPrivateQueueConcurrencyType
在一个私有队列中建立并管理 context。
3.NSMainQueueConcurrencyType
其实这种模式与第2种模式比较类似,只不过 context 与主队列绑定,同时也所以与应用的 event loop 紧密相连。当 context 与 UI 更新相关的话就使用这种模式。ios
从 iOS 9 开始就剩下后面两种模式了,那么搭建多线程 Core Data 环境的方案通常以下,建立一个 NSMainQueueConcurrencyType
的 context 用于响应 UI 事件,其余涉及大量数据操做可能会阻塞 UI 的,就使用 NSPrivateQueueConcurrencyType
的 context。git
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 进行数据操做就管这叫支持多线程了,那么应该怎么作呢?官方文档《Using a Private Queue to Support Concurrency》为咱们作了示范,在 private queue 的 context 中进行操做时,应该使用如下方法:github
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 的需求。关于这个话题,能够看这篇文章:《Concurrent Core Data Stacks – Performance Shootout》。swift
第二步:数据的同步操做
总的来讲,在多 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 间进行通讯呢?Core Data 提供了通知机制,context 执行保存时,会发出如下通知:
1.NSManagedObjectContextWillSaveNotification
Managed object context 即将执行保存操做时发出此通知,没有附带任何信息。
2.NSManagedObjectContextDidSaveNotification
Managed 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) } }
在这种方案下,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 的合并策略,决定了保存时合并数据发生冲突时如何应对,该属性有如下几种值:
1.NSErrorMergePolicy
默认策略,有冲突时保存失败,persistent store 和 context 都维持原样,并返回错误信息,是惟一反馈错误信息的合并策略。
2.NSMergeByPropertyStoreTrumpMergePolicy
当 persistent store 和 context 里的版本有冲突,persistent store 里的版本有优先权, context 里使用 persistent store 里的版本替换。
3.NSMergeByPropertyObjectTrumpMergePolicy
与上面相反,context 里的版本有优先权,persistent store 里使用 context 里的版本替换。
4.NSOverwriteMergePolicy
用 context 里的版本强制覆盖 persistent store 里的版本。
5.NSRollbackMergePolicy
放弃 context 中的全部变化并使用 persistent store 中的版本进行替换。
除了默认的 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 操做。实际上,采用这种方案不如上一个策略来得方便。
2.无论其余 context 中发生什么变化,当前 context 进行保存时直接覆盖 persistent store 里的版本,这种方案下 context 的合并策略须要采用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 的结构,能够参考这篇文章《Concurrent Core Data Stacks – Performance Shootout》,做者经过验证,证实了「设置一个 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 中的手动更新工做。
有一个疑问,好比一个backgroundContext 删除了几条数据 ,而对应的mainContext对这几天数据进行了操做;若是对mainContext进行了save,更新了到了PSC(persistent store),此时对backgroundContext进行了refreshAllObjects(),那删除的数据还在不?应对这个conflict, mergeChangesFromContextDidSaveNotification和refreshAllObjects(),有差异吗?仍是说,就是彻底还原了数据,而后再次进行数据操做...还有
if #available(iOS 9, *) {
//只影响 mainContext 中注册的 managed objects,但不引入 backgroundContext 新添加的对象。
mainContext.refreshAllObjects()
}
这段代码,若是说refreshAllObjects彻底还原了数据,后面没了数据操做,仍是没有保存成功,这还有意义吗
seedante: @fory2015 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];
}];
}
seedante: @华落 你在这个场景很简单的例子里这么作没有问题,其实这个例子里就不须要多线程 context,还有同步的麻烦。多线程 context 的例子能够看这里:http://objccn.io/issue-2-2/
华落: @seedante 感谢博主的回复,我大概看了一下代码,做者好像使用了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];
});
我这句可能跨线程访问了把- ,-
seedante: @华落 主线程只有一个,因此你在 backgroundContext 的私有队列里切换到主队列里刷新 UI 是没有问题的。你说『NSFetchedResultsController好像只能接受 mainqueue形式的context』,刷新 UI 只能在主线程里进行,若是 NSFetchedResultsController 发现绑定的 context 发生变化而刷新 UI 时不在主线程,一定有延迟和卡顿,你大概就是你说的好像只能接受mainqueue形式的context。