第一步:搭建 Core Data 多线程环境
这个问题首先要解决的是搭建 Core Data 多线程环境。Core Data 对并发模式的支持非常完备,NSManagedObjectContext 的指定初始化方法中就指定了并发模式:

init(concurrencyType ct: NSManagedObjectContextConcurrencyType)

有三种模式:
1.NSConfinementConcurrencyType
这种模式是用于向后兼容的,使用这种模式时你应该保证只能在创建的线程里使用 context,然而这不容易得到保证。关于此模式的最新消息是 iOS 9 中它将被废弃,不推荐使用。
2.NSPrivateQueueConcurrencyType
在一个私有队列中创建并管理 context。
3.NSMainQueueConcurrencyType
其实这种模式与第2种模式比较相似,只不过 context 与主队列绑定,同时也因此与应用的 event loop 紧密相连。当 context 与 UI 更新相关的话就使用这种模式。

从 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 进行数据操作就管这叫支持多线程了,那么应该怎么做呢?官方文档《Using a Private Queue to Support Concurrency》为我们做了示范,在 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 的需求。关于这个话题,可以看这篇文章:《Concurrent Core Data Stacks – Performance Shootout》

第二步:数据的同步操作
总的来说,在多 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.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)
}
}
多 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 的合并策略,决定了保存时合并数据发生冲突时如何应对,该属性有以下几种值:
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在发生冲突时返回错误等待下一步处理外,其他的合并策略直接根据自身的规则来处理合并冲突,因此在选择时要谨慎处理。从上面的解释来看,似乎NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy没什么区别,NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy也没有什么区别。区别在于怎么对待被覆盖的一方中没有冲突的变化(解释见此处)NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy采取的是局部替换,前者 context 中没有冲突的变化不会受到影响,后者 persistent store 中没有冲突的变化不受影响;NSOverwriteMergePolicyNSRollbackMergePolicy 采取的是全局替换,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 的合并策略采用NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,这样就省去了 reset 操作。实际上,采用这种方案不如上一个策略来得方便。
2.不管其他 context 中发生什么变化,当前 context 进行保存时直接覆盖 persistent store 里的版本,这种方案下 context 的合并策略需要采用NSOverwriteMergePolicyTypeNSMergeByPropertyObjectTrumpMergePolicy,而且执行保存时不会返回错误,不需要后续的处理。

小结

同步多个 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 中的手动更新工作。

文/seedante(简书作者)
原文链接:http://www.jianshu.com/p/37ab8f336f76
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

多线程操作Coredata(转)的更多相关文章

  1. C#中级-常用多线程操作(持续更新)

    一.前言       多线程操作一直是编程的常用操作,掌握好基本的操作可以让程序运行的更加有效.本文不求大而全,只是将我自己工作中常常用到的多线程操作做个分类和总结.平时记性不好的时候还能看看.本文参 ...

  2. java多线程操作

    进程是程序的一次动态的执行过程,它经历了从代码加载.执行完毕的一个完整过程,这个过程也是进程本身从产生.发展到最终消亡的过程. 多线程是实现并发机制的一种有效的手段.进程和线程一样,都是实现并发的一个 ...

  3. MFC的多线程操作

    记得用MFC做了一个图像自动修复软件,当时没有多线程操作这一概念,由于图像修复算法比较复杂,因此,当执行图像修复时,程序就像卡死了似得而不能做其他操作.其实MFC对这种情况有一种很好地解决方案,那就是 ...

  4. 更高效地提高redis client多线程操作的并发吞吐设计

    Redis是一个非常高效的基于内存的NOSQL数据库,它提供非常高效的数据读写效能.在实际应用中往往是带宽和CLIENT库读写损耗过高导致无法更好地发挥出Redis更出色的能力.下面结合一些redis ...

  5. C#多线程操作界面控件的解决方案(转)

    C#中利用委托实现多线程跨线程操作 - 张小鱼 2010-10-22 08:38 在使用VS2005的时候,如果你从非创建这个控件的线程中访问这个控件或者操作这个控件的话就会抛出这个异常.这是微软为了 ...

  6. c# BackGroundWorker 多线程操作的小例子

    在我们的程序中,经常会有一些耗时较长的运算,为了保证用户体验,不引起界面不响应,我们一般会采用多线程操作,让耗时操作在后台完成,完成后再进行处理或给出提示,在运行中,也会时时去刷新界面上的进度条等显示 ...

  7. swift语言之多线程操作和操作队列(下)———坚持51天吃掉大象(写技术文章)

    欢迎有兴趣的朋友,参与我的美女同事发起的活动<51天吃掉大象>,该美女真的很疯狂,希望和大家一起坚持51天做一件事情,我加入这个队伍,希望坚持51天每天写一篇技术文章.关注她的微信公众号: ...

  8. C# winform编程中多线程操作控件方法

    private void Form1_Load(object sender, EventArgs e) { Thread newthread = new Thread(new ThreadStart( ...

  9. sqlite:多线程操作数据库“database is locked”解决方法(二)

    上一篇博客<sqlite:多线程操作数据库“database is locked”解决方法>通过注册延时函数的方法来处理数据库被锁的问题.此方法固然能解决问题,但是在多个线程向数据库写入大 ...

随机推荐

  1. TSharding:用于蘑菇街交易平台的分库分表组件

    tsharding TSharding is the simple sharding component used in mogujie trade platform. 分库分表业界方案 分库分表TS ...

  2. 超实用的PHP代码片段

    一.查看邮件是否已被阅读 当你在发送邮件时,你或许很想知道该邮件是否被对方已阅读.这里有段非常有趣的代码片段能够显示对方IP地址记录阅读的实际日期和时间. 1 2 3 4 5 6 7 8 9 10 1 ...

  3. Android2.3.7源码结构分析

    对Andorid系统进行分析或者系统功能定制的时候,我们经常需要在众多文件中花费大量时间定位所需关注的部分.为了减轻这部分枯燥而不可避免的工作,本文对2.3.7版本的源码结构进行了简单分析.希望对刚加 ...

  4. CSS那些事儿-阅读随笔3(清除浮动)

    浮动主要是由浮动(float)属性导致的页面错位现象,清除浮动不仅能解决页面错位的现象,还可以解决子元素浮动导致父元素背景无法自适应子元素高度的问题.在CSS样式中,主要利用clear属性中的both ...

  5. bzoj1056: [HAOI2008]排名系统 && 1862: [Zjoi2006]GameZ游戏排名系统

    hash 加上 平衡树(名次树). 这道题麻烦的地方就在于输入的是一个名字,所以需要hash. 这个hash用的是向后探查避免冲突,如果用类似前向星的方式避免冲突,比较难写,容易挂掉,但也速度快些. ...

  6. 原创js模型驱动

    使用ajax方式请求数据,向页面展示一个对象的时候,往往让人头疼的是一大堆 .val()  .text()方法,这样做固然不会出错,但是效率太低 以下提供一个自己编写的Jquery模型驱动插件,正在测 ...

  7. BZOJ_1022_[SHOI2008]_小约翰的游戏John_(博弈论_反Nim游戏)

    描述 http://www.lydsy.com/JudgeOnline/problem.php?id=1022 反Nim游戏裸题.详见论文<组合游戏略述——浅谈SG游戏的若干拓展及变形>. ...

  8. poj2752 水题

    又2b了一次…… var s:ansistring; ans,pre:..] of longint; i,k,tot:longint; procedure main; begin pre[]:=;k: ...

  9. ZEat

    借助微博平台,记录每日饮食情况的Andorid程序. 项目地址:https://github.com/atskyline/ZEat 1.0.0APK下载地址:https://github.com/at ...

  10. [开发工具] 史上最全系列之开发环境搭建之DDMS

    原文链接:http://www.eoeandroid.com/forum.php?mod=viewthread&tid=275774 一.简介 DDMS 的全称是DalvikDebug Mon ...