认识CoreData - 多线程

该文章属于<简书 — 刘小壮>原创,转载请注明:

<简书 — 刘小壮> http://www.jianshu.com/p/283e67ba12a3git


CoreData使用相关的技术点已经讲差很少了,我所掌握的也就这么多了....

在本篇文章中主要讲CoreData的多线程,其中会包括并发队列类型、线程安全等技术点。我对多线程的理解可能不是太透彻,文章中出现的问题还请各位指出。在以后公司项目使用CoreData的过程当中,我会将其中遇到的多线程相关的问题更新到文章中。github

在文章的最后,会根据我对CoreData多线程的学习,以及在工做中的具体使用,给出一些关于多线程结构的设计建议,各位能够当作参考。sql

文章中若有疏漏或错误,还请各位及时提出,谢谢!😊数据库


博客配图

MOC并发队列类型

CoreDataMOC是支持多线程的,能够在建立MOC对象时,指定其并发队列的类型。当指定队列类型后,系统会将操做都放在指定的队列中执行,若是指定的是私有队列,系统会建立一个新的队列。但这都是系统内部的行为,咱们并不能获取这个队列,队列由系统所拥有,并由系统将任务派发到这个队列中执行的缓存

NSManagedObjectContext并发队列类型:
  • NSConfinementConcurrencyType : 若是使用init方法初始化上下文,默认就是这个并发类型。这个枚举值是不支持多线程的,从名字上也体现出来了。
  • NSPrivateQueueConcurrencyType : 私有并发队列类型,操做都是在子线程中完成的。
  • NSMainQueueConcurrencyType : 主并发队列类型,若是涉及到UI相关的操做,应该考虑使用这个枚举值初始化上下文。

其中NSConfinementConcurrencyType类型在iOS9以后已经被苹果废弃,不建议使用这个API。使用此类型建立的MOC,调用某些比较新的CoreDataAPI可能会致使崩溃。安全

MOC多线程调用方式

CoreDataMOC不是线程安全的,在多线程状况下使用MOC时,不能简单的将MOC从一个线程中传递到另外一个线程中使用,这并非CoreData的多线程,并且会出问题。对于MOC多线程的使用,苹果给出了本身的解决方案。数据结构

在建立的MOC中使用多线程,不管是私有队列仍是主队列,都应该采用下面两种多线程的使用方式,而不是本身手动建立线程。调用下面方法后,系统内部会将任务派发到不一样的队列中执行。能够在不一样的线程中调用MOC的这两个方法,这个是容许的。多线程

- (void)performBlock:(void (^)())block            异步执行的block,调用以后会马上返回。
- (void)performBlockAndWait:(void (^)())block     同步执行的block,调用以后会等待这个任务完成,才会继续向下执行。

下面是多线程调用的示例代码,在多线程的环境下执行MOCsave方法,就是将save方法放在MOCblock体中异步执行,其余方法的调用也是同样的。并发

[context performBlock:^{
    [context save:nil];
}];

可是须要注意的是,这两个block方法不能在NSConfinementConcurrencyType类型的MOC下调用,这个类型的MOC是不支持多线程的,只支持其余两种并发方式的MOC异步

多线程的使用

在业务比较复杂的状况下,须要进行大量数据处理,而且还须要涉及到UI的操做。对于这种复杂需求,若是都放在主队列中,对性能和界面流畅度都会有很大的影响,致使用户体验很是差,下降屏幕FPS。对于这种状况,能够采起多个MOC配合的方式。

CoreData多线程的发展中,在iOS5经历了一次比较大的变化,以后能够更方便的使用多线程。从iOS5开始,支持设置MOCparentContext属性,经过这个属性能够设置MOC父MOC。下面会针对iOS5以前和以后,分别讲解CoreData的多线程使用。

尽管如今的开发中早就不兼容iOS5以前的系统了,可是做为了解这里仍是要讲一下,并且这种同步方式在iOS5以后也是能够正常使用的,也有不少人还在使用这种同步方式,下面其余章节也是同理。

iOS5以前使用多个MOC

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

iOS5以后,MOC能够设置parentContext,一个parentContext能够拥有多个ChildContext。在ChildContext执行save操做后,会将操做pushparentContext,由parentContext去完成真正的save操做,而ChildContext全部的改变都会被parentContext所知晓,这解决了以前MOC手动同步数据的问题。

须要注意的是,在ChildContext调用save方法以后,此时并无将数据写入存储区,还须要调用parentContextsave方法。由于ChildContext并不拥有PSCChildContext也不须要设置PSC,因此须要parentContext调用PSC来执行真正的save操做。也就是只有拥有PSCMOC执行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以前进行数据同步

就像上面章节中讲到的,在iOS5以前存在多个MOC的状况下,一个MOC发生更改并提交存储区后,其余MOC并不知道这个改变,其余MOC和本地存储的数据是不一样步的,因此就涉及到数据同步的问题。

进行数据同步时,会遇到多种复杂状况。例如只有一个MOC数据发生了改变,其余MOC更新时并无对相同的数据作改变,这样不会形成冲突,能够直接将其余MOC更新。

若是在一个MOC数据发生改变后,其余MOC对相同的数据作了改变,并且改变的结果不一样,这样在同步时就会形成冲突。下面将会按照这两种状况,分别讲一下不一样状况下的冲突处理方式。

简单状况下的数据同步

简单状况下的数据同步,是针对于只有一个MOC的数据发生改变,并提交存储区后,其余MOC更新时并无对相同的数据作改变,只是单纯的同步数据的状况。

NSManagedObjectContext类中,根据不一样操做定义了一些通知。在一个MOC发生改变时,其余地方能够经过MOC中定义的通知名,来获取MOC发生的改变。在NSManagedObjectContext中定义了下面三个通知:

  • NSManagedObjectContextWillSaveNotification MOC将要向存储区存储数据时,调用这个通知。在这个通知中不能获取发生改变相关的NSManagedObject对象。
  • NSManagedObjectContextDidSaveNotification MOC向存储区存储数据后,调用这个通知。在这个通知中能够获取改变、添加、删除等信息,以及相关联的NSManagedObject对象。
  • NSManagedObjectContextObjectsDidChangeNotificationMOC中任何一个托管对象发生改变时,调用这个通知。例如修改托管对象的属性。

经过监听NSManagedObjectContextDidSaveNotification通知,获取全部MOCsave操做。

[[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返回值的选项。

  • NSErrorMergePolicy : 默认值,当出现合并冲突时,返回一个NSError对象来描述错误,而MOC和持久化存储区不发生改变
  • NSMergeByPropertyStoreTrumpMergePolicy : 以本地存储为准,使用本地存储来覆盖冲突部分。
  • NSMergeByPropertyObjectTrumpMergePolicy : 以MOC的为准,使用MOC来覆盖本地存储的冲突部分。
  • NSOverwriteMergePolicy : 以MOC为准,用MOC的全部NSManagedObject对象覆盖本地存储的对应对象。
  • NSRollbackMergePolicy : 以本地存储为准,MOC全部的NSManagedObject对象被本地存储的对应对象所覆盖。

上面五种策略中,除了第一个NSErrorMergePolicy的策略,其余四种中NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,以及NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy看起来是重复的。

其实它们并非冲突的,这四种策略的不一样体如今,对没有发生冲突的部分应该怎么处理NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy对没有冲突的部分,未冲突部分数据并不会受到影响。而NSRollbackMergePolicyNSOverwriteMergePolicy则是不管是否冲突,直接所有替换。

题外话:
对于MOC的这种合并策略来看,有木有感受到CoreData解决冲突的方式,和SVN解决冲突的方式特别像。。。

线程安全

不管是MOC仍是托管对象,都不该该在其余MOC的线程中执行操做,这两个API都不是线程安全的。但MOC能够在其余MOC线程中调用performBlock:方法,切换到本身的线程执行操做。

若是其余MOC想要拿到托管对象,并在本身的队列中使用托管对象,这是不容许的,托管对象是不能直接传递到其余MOC的线程的。可是能够经过获取NSManagedObjectNSManagedObjectID对象,在其余MOC中经过NSManagedObjectID对象,从持久化存储区中获取NSManagedObject对象,这样就是容许的。NSManagedObjectID是线程安全,而且能够跨线程使用的。

能够经过MOC获取NSManagedObjectID对应的NSManagedObject对象,例以下面几个MOCAPI

NSManagedObject *object = [context objectRegisteredForID:objectID];
NSManagedObject *object = [context objectWithID:objectID];

经过NSManagedObject对象的objectID属性,获取NSManagedObjectID类型的objectID对象。

NSManagedObjectID *objectID = object.objectID;

CoreData多线程结构设计

上面章节中写的大多都是怎么用CoreData多线程,在掌握多线程的使用后,就能够根据公司业务需求,设计一套CoreData多线程结构了。对于多线程结构的设计,应该本着尽可能减小主线程压力的角度去设计,将全部耗时操做都放在子线程中执行。

对于具体的设计我根据不一样的业务需求,给出两种设计方案的建议。

两层设计方案

在项目中多线程操做比较简单时,能够建立一个主队列mainMOC,和一个或多个私有队列的backgroundMOC。将全部backgroundMOCparentContext设置为mainMOC,采起这样的两层设计通常就可以知足大多数需求了。

两层设计方案

将耗时操做都放在backgroundMOC中执行,mainMOC负责全部和UI相关的操做。全部和UI无关的工做都交给backgroundMOC,在backgroundMOC对数据发生改变后,调用save方法会将改变pushmainMOC中,再由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同步时机

设置MOCparentContext属性以后,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];
    }];
}];

在上面这段代码中,mainMOCbackgroundMOCparentContext。在backgroundMOC执行save方法前,backgroundMOCmainMOC都不能获取到Employee的数据,在backgroundMOC执行完save方法后,自身上下文发生改变的同时,也将改变pushmainMOC中,mainMOC也具备了Employee对象。

因此在backgroundMOCsave方法执行时,是对内存中的上下文作了改变,当拥有PSCmainMOC执行save方法后,是对本地存储区作了改变。


好多同窗都问我有Demo没有,其实文章中贴出的代码组合起来就是个Demo。后来想了想,仍是给本系列文章配了一个简单的Demo,方便你们运行调试,后续会给全部博客的文章都加上Demo

Demo只是来辅助读者更好的理解文章中的内容,应该博客结合Demo一块儿学习,只看Demo仍是不能理解更深层的原理Demo中几乎每一行代码都会有注释,各位能够打断点跟着Demo执行流程走一遍,看看各个阶段变量的值。

Demo地址刘小壮的Github


这两天更新了一下文章,将CoreData系列的六篇文章整合在一块儿,作了一个PDF版的《CoreData Book》,放在我Github上了。PDF上有文章目录,方便阅读。

若是你以为不错,请把PDF帮忙转到其余群里,或者你的朋友,让更多的人了解CoreData,衷心感谢!😁

相关文章
相关标签/搜索