- 爲什麼使用多線程
- 如何使用多線程
- 多線程方案
到了這裏你必定會問,增刪改查功能已經實現了,用的好好的爲何要使用多線程呢?其實想想,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地址