データベースを扱うのに CoreDataは便利ですが、大量データの更新や保存をする際にはメインスレッドを妨害しないように別のスレッドで処理する必要があります。
ここでは CoreDataで非同期処理を行うための Tipsを紹介します。
元ネタは Multi-Context CoreData です。より詳しい解説や図解はこちらをどうぞ。
NSManagedObjectContext とマルチスレッド
NSManageObjectContext は CoreDataのデータオブジェクトを管理するクラスですが、このクラスはスレッドセーフではありません。このため、マルチスレッドで CoreDataのオブジェクトを扱えるようにするにはスレッドごとに NSManageObjectContextを用意する必要があります。
iOS 5以降では initWithConcurrencyType: に NSPrivateQueueConcurrencyType を指定して Contextオブジェクトを生成すると、Contextに対して専用のスレッドが割り当てられます。
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
performBlock: で指定したブロックは Contextが管理するスレッド上で実行されます。以下の Userオブジェクトの新規作成処理はメインスレッドではなく、temporaryContext用のスレッドで実行されます。
[temporaryContext performBlock:^{
NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:temporaryContext];
[managedObject setValue:@"hoge" forKey:@"name"];
}];
NSManagedObject間の更新通知
temporaryContextで行われた更新処理が行われると、更新内容に応じてメインスレッド上で UIの描画処理やデータベースへの永続化処理をする必要があります。
iOS 5以降の NSManagedObejctContext は、Context間で親子関係を持つことができ、子で行った更新を親に対して伝搬させられます。
以下のようにメインスレッド用のコンテキストを用意しておきます。
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
更新用の Contextにはメインコンテキストを親としてセットしておきます。
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = _mainContext;
temporaryContextに対して行った更新内容は、save: メソッドで親コンテキストに対して通知されマージされます。逆に言えば、save: メソッドをコールしなければマージされないため、編集操作の破棄などキャンセル処理に利用することもできます。この例では編集内容をメインコンテキストにマージし、その後、メインコンテキストで永続化処理を行っています。メインコンテキストでは UIの更新処理も行う必要があるでしょう。
[temporaryContext performBlock:^{
NSLog(@"データをバックグラウンドスレッドで更新する");
・・・
NSError *error;
NSLog(@"変更内容を main context にマージする");
if (![temporaryContext save:&error]) {
// handle error
}
[_mainContext performBlock:^{
NSLog(@"データを永続化する");
NSError *error;
if (![_mainContext save:&error]) {
// handle error
}
}];
}];
永続化処理の非同期化
ここまでの例ではオブジェクトの更新処理はバックグラウンドスレッドで実行していますが、永続化処理はメインスレッドでの実行になるため、その間、UIの動作が止まってしまいます。
永続化処理をバックグラウンドで実行するには保存操作専用の NSManagedObjectContext を用意します。このコンテキストを書き込みコンテキストと呼びます。
コンテキスト間の親子関係は以下のようになります。
書き込みコンテキスト
↑
メインコンテキスト
↑
更新コンテキスト
// 書き込みコンテキスト
_writerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
// メインコンテキスト
_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainContext.parentContext = _writerContext;
更新コンテキストで何か更新処理をすると変更内容がメインコンテキストに反映され メインスレッドで UIの描画処理を行います。さらにメインコンテキストにマージされた更新内容は、その親である書き込みコンテキストに伝搬してマージされ、バックグラウンドで永続化処理が行われます。
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = _mainContext;
[temporaryContext performBlock:^{
NSLog(@"データをバックグラウンドスレッドで更新する");
・・・
NSError *error;
NSLog(@"変更内容を main context にマージする");
if (![temporaryContext save:&error]) {
// handle error
}
[_mainContext performBlock:^{
NSLog(@"変更内容を writer context にマージする");
NSError *error;
if (![_mainContext save:&error]) {
// handle error
}
[_writerContext performBlock:^{
NSError *error;
NSLog(@"データを永続化する");
if (![_writerContext save:&error]) {
// handle error
}
}];
}];
}];
上記のようにすることで、メインスレッドから CoreDataの更新処理、永続化処理を分離でき、画面が固まるといった問題を回避できるようになります。また、更新処理を別のコンテキストに分離することで、更新処理を破棄する(save: しない)こともできるようになります。
以上。