- 为什么使用多线程
- 如何使用多线程
- 多线程方案
到了这里你必定会问,增删改查功能已经实现了,用的好好的为何要使用多线程呢?其实想想,Core Data毕竟是数据持久化技术,若是数据量大的话,使用主线程操做一定会产生线程拥塞。而UI的更新就是在主线程中进行的,这将会致使你的app界面“卡住”。此外当你须要同时执行多个操做时也须要使用多线程。ios
最初想法:
对于如何去实现,你首先可能会想到的是以下图的方案:实例化一个MOC对象,当有须要执行的操做时就开辟一个线程去执行。可是这样是不行的,因为MOC和MO不是线程安全的,对MO进行的操做和使用MOC进行的操做并不会上锁去保证操做的原子性。若是多线程共用MOC的话会出现数据混乱,甚至更严重的会致使程序崩溃。git
例如以下代码,先Add20条数据,再执行Update操做。下面的代码在屡次频繁执行时会crash。
咱们可以简单分析出来,因为MOC是同一个,因此在线程A中的for循环中执行时,有可能线程B已经执行完毕。在这种状况下,线程A中新增的一部分由MOC监听的MO对象会在线程B中被提早Save。这样的状况下两个操做混杂在了一块儿,严重的会产生crash。github
// 线程A执行Add操做 NSMutableArray *arr = [NSMutableArray array]; for (int i = 0; i < 20; i++) { [arr addObject:@{@"id": @"111", @"name": @"aaa"}]; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSManagedObjectContext *context = self.manager.moc; int i = 1; for (NSDictionary *params in arr) { User *user = [NSEntityDescription insertNewObjectForEntityForName:EntityName inManagedObjectContext:context]; user.userID = params[@"id"]; user.name = params[@"name"]; // 模拟在添加了5条数据以后,线程B执行完成Update操做 if (i == 5) { sleep(2); } i++; } [self.manager saveContext]; }); // 线程B执行Update操做 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSManagedObjectContext *context = self.manager.moc; NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:EntityName]; NSArray *resultArray = [context executeFetchRequest:fetchRequest error:nil]; for (User *user in resultArray) { user.name = @"newName"; } [self.manager saveContext]; });
正确的作法:macos
CoreData不是线程安全的(例子如上),对于ManagedObject以及ManagedObjectContext的访问都只能在对应的线程上进行,而不能跨线程。苹果推荐的作法是,一个线程使用一个NSManagedObjectContext对象。因为在每一个线程中的context是不一样的,并且它只管理本身监听的MO,context之间互不影响,因此不会出现context保存前它所监听的MO被其余context篡改或者提早提交的状况。segmentfault
API中提供的方法:安全
NSManagedObjectContext的类型:
实例化时提供了3种类型来方便进行多线程管理:多线程
NSConfinementConcurrencyType(iOS 9废弃)
NSPrivateQueueConcurrencyType
NSMainQueueConcurrencyType
NSManagedObjectContext提供的多线程执行方法:
API中提供了多线程执行方法,使得咱们不须要去本身维护线程队列或开启线程。并发
- (void)performBlock:(void (^)())block NS_AVAILABLE(10_7, 5_0); - (void)performBlockAndWait:(void (^)())block NS_AVAILABLE(10_7, 5_0);
1.对于NSConfinementConcurrencyType类型,iOS 9以后过时,context在实例化时并不会自动建立队列,须要本身管理多线程实现并发。当该类型的context使用上述的两个方法时会出现以下的crash。app
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Can only use -performBlock: on an NSManagedObjectContext that was created with a queue.
2.对于NSPrivateQueueConcurrencyType类型,该上下文会建立并管理一个私有队列(串行队列)。当你想要异步执行某个操做时,能够在performBlock方法的block中执行。异步
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; // 私有类型上下文执行performBlock方法 [privateContext performBlock:^{ NSLog(@"privateContext block: %@", [NSThread currentThread]); }]; // 至关于:串行队列 异步 执行block dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL); // 只建立一个queue与context绑定,每次都使用这一个queue dispatch_async(queue, ^{ NSLog(@"privateContext block: %@", [NSThread currentThread]); }); // 私有类型上下文执行performBlockAndWait方法 [privateContext performBlockAndWait:^{ NSLog(@"privateContext blockAndWait: %@", [NSThread currentThread]); }]; // 至关于:无队列线程操做,在当前线程中直接执行block NSLog(@"privateContext blockAndWait: %@", [NSThread currentThread]);
3.对于NSMainQueueConcurrencyType类型,该上下文会关联主队列。若是有UI对象执行的操做或者是须要在主线程中执行的操做,可使用该类型。
NSManagedObjectContext *mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; // 主类型上下文执行performBlock方法 [mainContext performBlock:^{ NSLog(@"mainContext block: %@", [NSThread currentThread]); }]; // 至关于:主队列 异步 执行block dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"mainContext block: %@", [NSThread currentThread]); }); // 主类型上下文执行performBlockAndWait方法 [mainContext performBlockAndWait:^{ NSLog(@"mainContext blockAndWait: %@", [NSThread currentThread]); }]; // 至关于在主线程中直接执行block if ([NSThread isMainThread]) { NSLog(@"mainContext blockAndWait: %@", [NSThread currentThread]); } else { dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"mainContext blockAndWait: %@", [NSThread currentThread]); }); }
tip:demo在最后
使用两个MOC,一个负责在后台处理各类耗时的操做,一个负责与UI进行协做。
存在的问题
咱们知道MOC和MO不是线程安全的,为了解决这个问题咱们在一个线程中仅使用一个MOC,不能跨线程访问同一个MOC和MO。可是这会存在问题。好比:使用一个context异步执行删除操做,首先查询,在查询出结果时恰好另外一个context更新了这些数据,删除操做在以后保存时是不知道数据被修改了,最终会致使删除失败。(该问题的研究,详见Demo中UserDao类的testMergeChanges方法)
为了解决这个问题,咱们须要使用通知来监听私有上下文的保存动做,并将更改的信息合并到其余上下文中:
// 上下文提交保存后的通知name NSManagedObjectContextDidSaveNotification
// 将通知中上下文提交的信息合并到执行该方法的上下文中 - (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification NS_AVAILABLE(10_5, 3_0);
经过创建上下文间的父子关系,避免上下文的合并操做。
iOS5.0以后新增了MOC之间的父子关系,子上下文的改动保存时会提交给父上下文,最后由根部的上下文提交全部改动给PSC。所以创建关系以后,上下文的改动就不须要用通知去告知其余上下文了。咱们能够经过设置以下属性来设置父上下文。
@property (nullable, strong) NSManagedObjectContext *parentContext API_AVAILABLE(macosx(10.7),ios(5.0));
方案二将使用三层的MOC去实现多线程Core Data,privateContext -> mainContext -> rootContext。
其中privateContext用于执行操做,mainContext用于与UI协做,rootContext用于在后台保存全部子上下文的提交。
存在的问题
MO都有惟一的MOID与之对应,为了不实例化MO时消耗大量资源来确保ID的惟一性,因此MO在实例化时会被给予一个临时的ID,这个ID在MOC范围内惟一。当MOC进行提交时,须要将临时ID转化为全局ID,因此咱们须要监听MOC将要保存的通知来处理MOID的转换:
// 上下文将要提交保存的通知name NSManagedObjectContextWillSaveNotification
// MOID转换方法 - (BOOL)obtainPermanentIDsForObjects:(NSArray<NSManagedObject *> *)objects error:(NSError **)error NS_AVAILABLE(10_5, 3_0);
方案一初始化:
CoreDataManager.m:
方案二初始化:
CoreDataManager.m:
公共辅助方法:
UserDao.m:
增:
删:
改:
查:
CoreData整理(一)——基本概念与简单使用
CoreData整理(三)——MagicalRecord的使用
CoreData整理(四)——数据迁移和其余问题
Demo地址