<简书 — 刘小壮> http://www.jianshu.com/p/283e67ba12a3git
CoreData
使用相关的技术点已经讲差很少了,我所掌握的也就这么多了....在本篇文章中主要讲
CoreData
的多线程,其中会包括并发队列类型、线程安全等技术点。我对多线程的理解可能不是太透彻,文章中出现的问题还请各位指出。在以后公司项目使用CoreData
的过程当中,我会将其中遇到的多线程相关的问题更新到文章中。github在文章的最后,会根据我对
CoreData
多线程的学习,以及在工做中的具体使用,给出一些关于多线程结构的设计建议,各位能够当作参考。sql文章中若有疏漏或错误,还请各位及时提出,谢谢!😊数据库
在CoreData
中MOC
是支持多线程的,能够在建立MOC
对象时,指定其并发队列的类型。当指定队列类型后,系统会将操做都放在指定的队列中执行,若是指定的是私有队列,系统会建立一个新的队列。但这都是系统内部的行为,咱们并不能获取这个队列,队列由系统所拥有,并由系统将任务派发到这个队列中执行的。缓存
init
方法初始化上下文,默认就是这个并发类型。这个枚举值是不支持多线程的,从名字上也体现出来了。UI
相关的操做,应该考虑使用这个枚举值初始化上下文。其中NSConfinementConcurrencyType
类型在iOS9
以后已经被苹果废弃,不建议使用这个API
。使用此类型建立的MOC
,调用某些比较新的CoreData
的API
可能会致使崩溃。安全
在CoreData
中MOC不是线程安全的,在多线程状况下使用MOC
时,不能简单的将MOC
从一个线程中传递到另外一个线程中使用,这并非CoreData
的多线程,并且会出问题。对于MOC
多线程的使用,苹果给出了本身的解决方案。数据结构
在建立的MOC
中使用多线程,不管是私有队列仍是主队列,都应该采用下面两种多线程的使用方式,而不是本身手动建立线程。调用下面方法后,系统内部会将任务派发到不一样的队列中执行。能够在不一样的线程中调用MOC
的这两个方法,这个是容许的。多线程
- (void)performBlock:(void (^)())block 异步执行的block,调用以后会马上返回。 - (void)performBlockAndWait:(void (^)())block 同步执行的block,调用以后会等待这个任务完成,才会继续向下执行。
下面是多线程调用的示例代码,在多线程的环境下执行MOC
的save
方法,就是将save
方法放在MOC
的block
体中异步执行,其余方法的调用也是同样的。并发
[context performBlock:^{ [context save:nil]; }];
可是须要注意的是,这两个block
方法不能在NSConfinementConcurrencyType
类型的MOC
下调用,这个类型的MOC
是不支持多线程的,只支持其余两种并发方式的MOC
。异步
在业务比较复杂的状况下,须要进行大量数据处理,而且还须要涉及到UI
的操做。对于这种复杂需求,若是都放在主队列中,对性能和界面流畅度都会有很大的影响,致使用户体验很是差,下降屏幕FPS
。对于这种状况,能够采起多个MOC
配合的方式。
CoreData
多线程的发展中,在iOS5
经历了一次比较大的变化,以后能够更方便的使用多线程。从iOS5
开始,支持设置MOC
的parentContext
属性,经过这个属性能够设置MOC
的父MOC
。下面会针对iOS5
以前和以后,分别讲解CoreData
的多线程使用。
尽管如今的开发中早就不兼容iOS5
以前的系统了,可是做为了解这里仍是要讲一下,并且这种同步方式在iOS5
以后也是能够正常使用的,也有不少人还在使用这种同步方式,下面其余章节也是同理。
在iOS5
以前实现MOC
的多线程,能够建立多个MOC
,多个MOC
使用同一个PSC
,并让多个MOC
实现数据同步。经过这种方式不用担忧PSC
在调用过程当中的线程问题,MOC
在使用PSC
进行save
操做时,会对PSC
进行加锁,等当前加锁的MOC
执行完操做以后,其余MOC
才能继续执行操做。
每个PSC
都对应着一个持久化存储区,PSC
知道存储区中数据存储的数据结构,而MOC
须要使用这个PSC
进行save
操做的实现。
这样作有一个问题,当一个MOC
发生改变并持久化到本地时,系统并不会将其余MOC
缓存在内存中的NSManagedObject
对象改变。因此这就须要咱们在MOC
发生改变时,将其余MOC
数据更新。
根据上面的解释,在下面例子中建立了一个主队列的mainMOC
,主要用于UI
操做。一个私有队列的backgroundMOC
,用于除UI
以外的耗时操做,两个MOC
使用的同一个PSC
。
// 获取PSC实例对象 - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { // 建立托管对象模型,并指明加载Company模型文件 NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Company" withExtension:@"momd"]; NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath]; // 建立PSC对象,并将托管对象模型当作参数传入,其余MOC都是用这一个PSC。 NSPersistentStoreCoordinator *PSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; // 根据指定的路径,建立并关联本地数据库 NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject; dataPath = [dataPath stringByAppendingFormat:@"/%@.sqlite", @"Company"]; [PSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil]; return PSC; } // 初始化用于本地存储的全部MOC - (void)createManagedObjectContext { // 建立PSC实例对象,其余MOC都用这一个PSC。 NSPersistentStoreCoordinator *PSC = self.persistentStoreCoordinator; // 建立主队列MOC,用于执行UI操做 NSManagedObjectContext *mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; mainMOC.persistentStoreCoordinator = PSC; // 建立私有队列MOC,用于执行其余耗时操做 NSManagedObjectContext *backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; backgroundMOC.persistentStoreCoordinator = PSC; // 经过监听NSManagedObjectContextDidSaveNotification通知,来获取全部MOC的改变消息 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:nil]; } // MOC改变后的通知回调 - (void)contextChanged:(NSNotification *)noti { NSManagedObjectContext *MOC = noti.object; // 这里须要作判断操做,判断当前改变的MOC是否咱们将要作同步的MOC,若是就是当前MOC本身作的改变,那就不须要再同步本身了。 // 因为项目中可能存在多个PSC,因此下面还须要判断PSC是否当前操做的PSC,若是不是当前PSC则不须要同步,不要去同步其余本地存储的数据。 [MOC performBlock:^{ // 直接调用系统提供的同步API,系统内部会完成同步的实现细节。 [MOC mergeChangesFromContextDidSaveNotification:noti]; }]; }
在上面的Demo
中,建立了一个PSC
,并将其余MOC
都关联到这个PSC
上,这样全部的MOC
执行本地持久化相关的操做时,都是经过同一个PSC
进行操做的。并在下面添加了一个通知,这个通知是监听全部MOC
执行save
操做后的通知,并在通知的回调方法中进行数据的合并。
在iOS5
以后,MOC
能够设置parentContext
,一个parentContext
能够拥有多个ChildContext
。在ChildContext
执行save
操做后,会将操做push
到parentContext
,由parentContext
去完成真正的save
操做,而ChildContext
全部的改变都会被parentContext
所知晓,这解决了以前MOC
手动同步数据的问题。
须要注意的是,在ChildContext
调用save
方法以后,此时并无将数据写入存储区,还须要调用parentContext
的save
方法。由于ChildContext
并不拥有PSC
,ChildContext
也不须要设置PSC
,因此须要parentContext
调用PSC
来执行真正的save
操做。也就是只有拥有PSC
的MOC
执行save
操做后,才是真正的执行了写入存储区的操做。
- (void)createManagedObjectContext { // 建立PSC实例对象,仍是用上面Demo的实例化代码 NSPersistentStoreCoordinator *PSC = self.persistentStoreCoordinator; // 建立主队列MOC,用于执行UI操做 NSManagedObjectContext *mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; mainMOC.persistentStoreCoordinator = PSC; // 建立私有队列MOC,用于执行其余耗时操做,backgroundMOC并不须要设置PSC NSManagedObjectContext *backgroundMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; backgroundMOC.parentContext = mainMOC; // 私有队列的MOC和主队列的MOC,在执行save操做时,都应该调用performBlock:方法,在本身的队列中执行save操做。 // 私有队列的MOC执行完本身的save操做后,还调用了主队列MOC的save方法,来完成真正的持久化操做,不然不能持久化到本地 [backgroundMOC performBlock:^{ [backgroundMOC save:nil]; [mainMOC performBlock:^{ [mainMOC save:nil]; }]; }]; }
上面例子中建立一个主队列的mainMOC
,来完成UI
相关的操做。建立私有队列的backgroundMOC
,处理复杂逻辑以及数据处理操做,在实际开发中能够根据需求建立多个backgroundMOC
。须要注意的是,在backgroundMOC
执行完save
方法后,又在mainMOC
中执行了一次save
方法,这步是很重要的。
就像上面章节中讲到的,在iOS5
以前存在多个MOC
的状况下,一个MOC
发生更改并提交存储区后,其余MOC
并不知道这个改变,其余MOC
和本地存储的数据是不一样步的,因此就涉及到数据同步的问题。
进行数据同步时,会遇到多种复杂状况。例如只有一个MOC
数据发生了改变,其余MOC
更新时并无对相同的数据作改变,这样不会形成冲突,能够直接将其余MOC
更新。
若是在一个MOC
数据发生改变后,其余MOC
对相同的数据作了改变,并且改变的结果不一样,这样在同步时就会形成冲突。下面将会按照这两种状况,分别讲一下不一样状况下的冲突处理方式。
简单状况下的数据同步,是针对于只有一个MOC
的数据发生改变,并提交存储区后,其余MOC
更新时并无对相同的数据作改变,只是单纯的同步数据的状况。
在NSManagedObjectContext
类中,根据不一样操做定义了一些通知。在一个MOC
发生改变时,其余地方能够经过MOC
中定义的通知名,来获取MOC
发生的改变。在NSManagedObjectContext
中定义了下面三个通知:
MOC
将要向存储区存储数据时,调用这个通知。在这个通知中不能获取发生改变相关的NSManagedObject
对象。MOC
向存储区存储数据后,调用这个通知。在这个通知中能够获取改变、添加、删除等信息,以及相关联的NSManagedObject
对象。MOC
中任何一个托管对象发生改变时,调用这个通知。例如修改托管对象的属性。经过监听NSManagedObjectContextDidSaveNotification
通知,获取全部MOC
的save
操做。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(settingsContext:) name:NSManagedObjectContextDidSaveNotification object:nil];
不须要在通知的回调方法中,编写代码对比被修改的托管对象。MOC
为咱们提供了下面的方法,只须要将通知对象传入,系统会自动同步数据。
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification;
下面是通知中的实现代码,可是须要注意的是,因为通知是同步执行的,在通知对应的回调方法中所处的线程,和发出通知的MOC
执行操做时所处的线程是同一个线程,也就是系统performBlock:
回调方法分配的线程。
因此其余MOC
在通知回调方法中,须要注意使用performBlock:
方法,并在block
体中执行操做。
- (void)settingsContext:(NSNotification *)noti { [context performBlock:^{ // 调用须要同步的MOC对象的merge方法,直接将通知对象当作参数传进去便可,系统会完成同步操做。 [context mergeChangesFromContextDidSaveNotification:noti]; }]; }
在一个MOC
对本地存储区的数据发生改变,而其余MOC
也对一样的数据作了改变,这样后面执行save
操做的MOC
就会冲突,并致使后面的save
操做失败,这就是复杂状况下的数据合并。
这是由于每次一个MOC
执行一次fetch
操做后,会保存一个本地持久化存储的状态,当下次执行save
操做时会对比这个状态和本地持久化状态是否同样。若是同样,则表明本地没有其余MOC
对存储发生过改变;若是不同,则表明本地持久化存储被其余MOC
改变过,这就是形成冲突的根本缘由。
对于这种冲突的状况,能够经过MOC
对象指定解决冲突的方案,经过mergePolicy
属性来设置方案。mergePolicy
属性有下面几种可选的策略,默认是NSErrorMergePolicy
方式,这也是惟一一个有NSError
返回值的选项。
NSError
对象来描述错误,而MOC
和持久化存储区不发生改变。MOC
的为准,使用MOC
来覆盖本地存储的冲突部分。MOC
为准,用MOC
的全部NSManagedObject
对象覆盖本地存储的对应对象。MOC
全部的NSManagedObject
对象被本地存储的对应对象所覆盖。上面五种策略中,除了第一个NSErrorMergePolicy
的策略,其余四种中NSMergeByPropertyStoreTrumpMergePolicy
和NSRollbackMergePolicy
,以及NSMergeByPropertyObjectTrumpMergePolicy
和NSOverwriteMergePolicy
看起来是重复的。
其实它们并非冲突的,这四种策略的不一样体如今,对没有发生冲突的部分应该怎么处理。NSMergeByPropertyStoreTrumpMergePolicy
和NSMergeByPropertyObjectTrumpMergePolicy
对没有冲突的部分,未冲突部分数据并不会受到影响。而NSRollbackMergePolicy
和NSOverwriteMergePolicy
则是不管是否冲突,直接所有替换。
题外话:
对于MOC
的这种合并策略来看,有木有感受到CoreData
解决冲突的方式,和SVN
解决冲突的方式特别像。。。
不管是MOC
仍是托管对象,都不该该在其余MOC
的线程中执行操做,这两个API都不是线程安全的。但MOC
能够在其余MOC
线程中调用performBlock:
方法,切换到本身的线程执行操做。
若是其余MOC
想要拿到托管对象,并在本身的队列中使用托管对象,这是不容许的,托管对象是不能直接传递到其余MOC
的线程的。可是能够经过获取NSManagedObject
的NSManagedObjectID
对象,在其余MOC
中经过NSManagedObjectID
对象,从持久化存储区中获取NSManagedObject
对象,这样就是容许的。NSManagedObjectID
是线程安全,而且能够跨线程使用的。
能够经过MOC
获取NSManagedObjectID
对应的NSManagedObject
对象,例以下面几个MOC
的API
。
NSManagedObject *object = [context objectRegisteredForID:objectID]; NSManagedObject *object = [context objectWithID:objectID];
经过NSManagedObject
对象的objectID
属性,获取NSManagedObjectID
类型的objectID
对象。
NSManagedObjectID *objectID = object.objectID;
上面章节中写的大多都是怎么用CoreData
多线程,在掌握多线程的使用后,就能够根据公司业务需求,设计一套CoreData
多线程结构了。对于多线程结构的设计,应该本着尽可能减小主线程压力的角度去设计,将全部耗时操做都放在子线程中执行。
对于具体的设计我根据不一样的业务需求,给出两种设计方案的建议。
在项目中多线程操做比较简单时,能够建立一个主队列mainMOC
,和一个或多个私有队列的backgroundMOC
。将全部backgroundMOC
的parentContext
设置为mainMOC
,采起这样的两层设计通常就可以知足大多数需求了。
将耗时操做都放在backgroundMOC
中执行,mainMOC
负责全部和UI
相关的操做。全部和UI
无关的工做都交给backgroundMOC
,在backgroundMOC
对数据发生改变后,调用save
方法会将改变push
到mainMOC
中,再由mainMOC
执行save
方法将改变保存到存储区。
代码这里就不写了,和上面例子中设置parentContext
代码同样,主要讲一下设计思路。
可是咱们发现,上面的save
操做最后仍是由mainMOC
去执行的,backgroundMOC
只是负责处理数据。虽然mainMOC
只执行save
操做并不会很耗时,可是若是save
涉及的数据比较多,这样仍是会对性能形成影响的。
虽然客户端不多涉及到大量数据处理的需求,可是假设有这样的需求。能够考虑在两层结构之上,给mainMOC
之上再添加一个parentMOC
,这个parentMOC
也是私有队列的MOC
,用于处理save
操做。
这样CoreData
存储的结构就是三层了,最底层是backgroundMOC
负责处理数据,中间层是mainMOC
负责UI
相关操做,最上层也是一个backgroundMOC
负责执行save
操做。这样就将影响UI
的全部耗时操做全都剥离到私有队列中执行,使性能达到了很好的优化。
须要注意的是,执行MOC
相关操做时,不要阻塞当前主线程。全部MOC
的操做应该是异步的,不管是子线程仍是主线程,尽可能少的使用同步block
方法。
设置MOC
的parentContext
属性以后,parent
对于child
的改变是知道的,可是child
对于parent
的改变是不知道的。苹果这样设计,应该是为了更好的数据同步。
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:backgroundMOC]; emp.name = @"lxz"; emp.brithday = [NSDate date]; emp.height = @1.7f; [backgroundMOC performBlock:^{ [backgroundMOC save:nil]; [mainMOC performBlock:^{ [mainMOC save:nil]; }]; }];
在上面这段代码中,mainMOC
是backgroundMOC
的parentContext
。在backgroundMOC
执行save
方法前,backgroundMOC
和mainMOC
都不能获取到Employee
的数据,在backgroundMOC
执行完save
方法后,自身上下文发生改变的同时,也将改变push
到mainMOC
中,mainMOC
也具备了Employee
对象。
因此在backgroundMOC
的save
方法执行时,是对内存中的上下文作了改变,当拥有PSC
的mainMOC
执行save
方法后,是对本地存储区作了改变。
好多同窗都问我有Demo
没有,其实文章中贴出的代码组合起来就是个Demo
。后来想了想,仍是给本系列文章配了一个简单的Demo
,方便你们运行调试,后续会给全部博客的文章都加上Demo
。
Demo
只是来辅助读者更好的理解文章中的内容,应该博客结合Demo
一块儿学习,只看Demo
仍是不能理解更深层的原理。Demo
中几乎每一行代码都会有注释,各位能够打断点跟着Demo
执行流程走一遍,看看各个阶段变量的值。
Demo地址:刘小壮的Github
这两天更新了一下文章,将CoreData
系列的六篇文章整合在一块儿,作了一个PDF
版的《CoreData Book》,放在我Github上了。PDF
上有文章目录,方便阅读。
若是你以为不错,请把PDF帮忙转到其余群里,或者你的朋友,让更多的人了解CoreData,衷心感谢!😁