1. 程式人生 > >CoreData 從入門到精通(四)併發操作

CoreData 從入門到精通(四)併發操作

通常情況下,CoreData 的增刪改查操作都在主執行緒上執行,那麼對資料庫的操作就會影響到 UI 操作,這在操作的資料量比較小的時候,執行的速度很快,我們也不會察覺到對 UI 的影響,但是當資料量特別大的時候,再把 CoreData 的操作放到主執行緒中就會影響到 UI 的流暢性。自然而然地我們就會想到使用後臺執行緒來處理大量的資料操作。

使用後臺 managedObjectContext

CoreData 裡使用後臺更新資料最常用的方案是一個 persistentStoreCoordinator 持久化儲存協調器對應兩個 managedObjectContext 管理上下文,NSManagedObjectContext

在建立時,可以傳入 ConcurrencyType 來指定 context 的併發型別。指定 NSMainQueueConcurrencyType 就是我們平時建立的執行在主佇列的 context;指定成 NSPrivateQueueConcurrencyType 的話,context 就會執行在它所管理的一個私有佇列中;另外還有 NSConfinementConcurrencyType 是適用於舊裝置的併發型別,現在已經被廢棄了,所以實際上只有兩種併發型別。
下面是建立 backgroundContext 的程式碼:

NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator = self.persistentStoreCoordinator;

在最新的 iOS 10 中,CoreData 棧的建立被封裝在了 NSPersistentContainer 類中,用它來建立 backgroundContext 更加簡單:

NSManagedObjectContext *backgroundContext = ((AppDelegate *)[UIApplication sharedApplication].delegate).persistentContainer.newBackgroundContext
;

另外,後臺 context 的操作得放在 performBlockperformBlockAndWait 方法裡執行,performBlock 會非同步的執行,不會阻塞當前的執行緒,而 performBlockAndWait 則會阻塞當前的執行緒直到方法返回才會繼續向下執行。下面是一段後臺插入資料的示例程式碼:

[self.backgroundContext performBlock:^{

    for (NSUInteger i = 0; i < 100000; i++) {

        NSString *name = [NSString stringWithFormat:@"student-%d", arc4random_uniform(9999)];
      int16_t age = arc4random_uniform(10) + 10;
      int16_t stuId = arc4random_uniform(9999);
      Student *student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:self.backgroundContext];
      student.studentName = name;
      student.studentAge = age;
      student.studentId = stuId;

    }
    NSError *error;
    [self.backgroundContext save:&error];
    [self.logger dealWithError:error
                      whenFail:@"failed to insert"
                   whenSuccess:@"insert success"];

}];

後臺插入資料之後,還沒有完,因為資料是通過後臺的 context 寫入到本地的持久化資料庫的,所以這時候主佇列的 context 是不知道本地資料變化的,所以還需要通知到主佇列的 context:“資料庫的內容有變化啦,看看你有沒有需要合併的”。這個過程可以通過監聽一條通知來實現。這個通知就是 NSManagedObjectContextDidSaveNotification,在每次呼叫 NSManagedObjectContextsave:方法時都會自動傳送,通知中的 userInfo 中包含了修改的資料,可以通過 NSInsertedObjectsKeyNSUpdatedObjectsKeyNSDeletedObjectsKey 這三個 key 獲取到。
-w600

收到通知之後,只需要呼叫 [self.mainContext mergeChangesFromContextDidSaveNotification:note] 就可以將修改的資料合併到主執行緒的 context

下面是示例程式碼:

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveContextSave:) name:NSManagedObjectContextDidSaveNotification object:self.backgroundContext];
}

- (void)doSometingInsertingInBackground {
    // backgroundContext ....
}

- (void)receiveContextSave:(NSNotification *)note {
    [self.context mergeChangesFromContextDidSaveNotification:note];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

注意:通知的 userInfo 裡儲存的 managedObjects 不可以直接在另一個執行緒的 context 中直接使用!也就是 managedObject 不是跨執行緒的,如果想要在別的執行緒操作,必須通過 objectId 在另一個 context 裡再重新獲得這個 object

- (void)receiveContextSave:(NSNotification *)note {

    [self.context mergeChangesFromContextDidSaveNotification:note];

    NSSet<Student *> *managedObjects = note.userInfo[NSInsertedObjectsKey];
    NSManagedObjectID *studentId = managedObjects.allObjects[0].objectID;

    [self.context performBlock:^{

        // 這是錯的
        // Student *wrongStudent = managedObjects.allObjects[0];

        // 應該這麼做
        Student *student = [self.context objectWithID:studentId];
        // modify student...
    }];
}