CoreData整理(二)——多執行緒方案

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

CoreData整理(二)——多執行緒方案


目錄

  • 為何使用多執行緒
  • 如何使用多執行緒
  • 多執行緒方案

為何使用多執行緒

    到了這裡你一定會問,增刪改查功能已經實現了,用的好好的為什麼要使用多執行緒呢?其實想一想,Core Data畢竟是資料持久化技術,如果資料量大的話,使用主執行緒操作必定會產生執行緒擁塞。而UI的更新就是在主執行緒中進行的,這將會導致你的app介面“卡住”。此外當你需要同時執行多個操作時也需要使用多執行緒。


如何使用多執行緒

最初想法:
    對於如何去實現,你首先可能會想到的是如下圖的方案:例項化一個MOC物件,當有需要執行的操作時就開闢一個執行緒去執行。但是這樣是不行的,由於MOC和MO不是執行緒安全的,對MO進行的操作和使用MOC進行的操作並不會上鎖去保證操作的原子性。如果多執行緒共用MOC的話會出現資料混亂,甚至更嚴重的會導致程式崩潰。

例如如下程式碼,先Add20條資料,再執行Update操作。下面的程式碼在多次頻繁執行時會crash。
我們能夠簡單分析出來,由於MOC是同一個,所以線上程A中的for迴圈中執行時,有可能執行緒B已經執行完畢。在這種情況下,執行緒A中新增的一部分由MOC監聽的MO物件會線上程B中被提前Save。這樣的情況下兩個操作混雜在了一起,嚴重的會產生crash。

// 執行緒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];
});

正確的做法:

CoreData不是執行緒安全的(例子如上),對於ManagedObject以及ManagedObjectContext的訪問都只能在對應的執行緒上進行,而不能跨執行緒。蘋果推薦的做法是,一個執行緒使用一個NSManagedObjectContext物件。由於在每個執行緒中的context是不同的,而且它只管理自己監聽的MO,context之間互不影響,所以不會出現context儲存前它所監聽的MO被其他context篡改或者提前提交的情況。

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。

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 blockwait: %@", [NSThread currentThread]);
}];
// 相當於:序列佇列 同步 執行block(在當前執行緒中執行)
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);  只建立一個queue與context繫結,每次都使用這一個queue
dispatch_sync(queue, ^{
    NSLog(@"privateContext blockwait: %@", [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 blockWait: %@", [NSThread currentThread]);
}];
// 相當於在主執行緒中直接執行block
if ([NSThread isMainThread]) {
    NSLog(@"mainContext blockWait: %@", [NSThread currentThread]);
} else {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"mainContext blockWait: %@", [NSThread currentThread]);
    });
}

官方文件


多執行緒方案

tip:demo在最後

方案一

使用兩個MOC,一個負責在後臺處理各種耗時的操作,一個負責與UI進行協作。

    我們知道MOC和MO不是執行緒安全的,為了解決這個問題我們在一個執行緒中僅使用一個MOC,不能跨執行緒訪問同一個MOC和MO。但是這會存在問題。
    比如:我在後臺執行緒中執行update操作,主執行緒中的上下文是不知道的,所以當我在主執行緒中去進行查詢時,查詢出來的結果並不是已更新後的結果。

為了解決這個問題,我們需要使用通知來監聽私有上下文的儲存動作,並將更改的資訊合併到其他上下文中:

// 上下文提交儲存後的通知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地址


參考文章

Core Data 執行緒大揭祕
iOSCoreData詳解(五)多執行緒

相關文章

IOS開發 最新文章