このスライドはAkiba.swift(2018/2/23)の発表資料です
どんな発表するか案が3つありました
- やはりお前らのCore Dataは間違っている。続
- 地獄を揺さぶる(ヘルシェイク)Core Data
- まだSwaggerで消耗してるの?
最初に
自分の経験上、煽ったタイトルにすると、読んだ人は何か一言物申したくなって、そのためには揚げ足を取ろうと平常心を欠いたことでも発言したくなるので、もし、煽ったタイトルで広まることを目的としても、煽りタイトルはおすすめできません。
もっと多くの人に広めたいという気持ちは分かる
私も趣味のMacアプリは作りはなるべくリファレンスをみないでやっていたりますが、そういうふうに間違った使い方をしている人って大抵動けば良くて公式リファレンスを読まずにやっていません。
そういう人にも広まるようにタイトルを工夫して煽ってしまう気持ちわかります。
ちゃんとしたフィードバックがほしいなら
タイトルで煽ってはいけない。さらに本文で煽ったらもっとダメ。
本題
話すこと
- Core Dataの使い方を知るには、リファレンスだけ読んでやっていくのは厳しい
- Magical RecordというCore Dataラッパーライブラリのコードを読むと理解が深まる
- しかしMagical Recordは使わないほうがいいよ
Core Data のリファレンスがきびしい
-
AppleのリファレンスにCore Dataに関するいろいろなTipsが日本語で書かれています
- このリファレンス結構読むのがしんどいです
- Core Dataは多機能なので専門用語が多い
- Core Dataは2006年くらいにはあったらしくそのためモダンなインターフェースではない
- パフォーマンスのための並列処理についても言及されている
- このリファレンス結構読むのがしんどいです
Core Dataの難問その一、「NSManagedObjectContext の概念がきびしい」
- 世の中にあるWebアプリケーションには
NSManagedObjectContext
とそのまま同じ概念はないのでピンとこない - クライアントサイドのアプリケーションでも、
NSManagedObjectContext
のような概念は隠蔽されがち
しかし、NSManagedObjectContext
が出来る事が何かを知ると理解しやすい
- 永続化のデータをメモリ上でコントロールできる
- ロールバックできる
- undoやredoができる(使ったことないですが)
- 多量のデータの書き込みの必要があっても、データ書き込み用の
NSManagedObjectContext
を作れる- メインスレッドに処理させずにユーザは快適に使える
- 読み取り用の
NSManagedObjectContext
を作れる
この便利な NSManagedObjectContext
をMagical Recordがどのように作るかを知るともっとよく理解できます
用語について
-
NSManagedObjectContext
はcontext - Magical Recordは MR と書いていきます
MRはどうやってcontextを作って使ってる?
- 全てのcontextの親になるrootContextを作る
- rootContextから読み取り用のdefaultContextをつくって基本読み取りはそれを使う
- rootContextもdefaultContextもシングルトンで呼び出す
- 書き込みには書き込み用のcontextをrootContextから作成する
context親子の図
- 親: rootContext
- 子: 読み込み用 defaultContext
- 子: 書き込み用 context
読み取り用のdefaultContextとは何か
defaultContextの初期化メソッド
+ (void) MR_initializeDefaultContextWithCoordinator:(NSPersistentStoreCoordinator *)coordinator;
{
NSAssert(coordinator, @"Provided coordinator cannot be nil!");
if (MagicalRecordDefaultContext == nil)
{
NSManagedObjectContext *rootContext = [self MR_contextWithStoreCoordinator:coordinator];
[self MR_setRootSavingContext:rootContext];
NSManagedObjectContext *defaultContext = [self MR_newMainQueueContext];
[self MR_setDefaultContext:defaultContext];
[defaultContext setParentContext:rootContext];
}
}
- まずrootContextを作って登録
MR_setRootSavingContext
- defaultContextをメインキューで使うようにして作る
MR_newMainQueueContext
- 作成したcontextをdefaultContextとしてシングルトンで呼び出せる登録
MR_setDefaultContext
- defaultContextの親をrootContの親をにする
setParentContext
defaultContextをメインキューで使うようにして作る MR_newMainQueueContext
+ (NSManagedObjectContext *) MR_newMainQueueContext
{
NSManagedObjectContext *context = [[self alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
MRLogInfo(@"Created new main queue context: %@", context);
return context;
}
NSMainQueueConcurrencyType というのでメインキュー用になる
NSMainQueueConcurrencyTypeはAppleのドキュメントから引用すると
NSMainQueueConcurrencyTypeはアプリケーションインターフェイスでの使用に特化したもので、アプリケーションのメインキューでのみ使うことができます。
よくわからんけど
- アプリケーションインターフェイスでの使用に特化したもの
- つまりメインスレッド用
- メインキューでのみ使う
- まぁつまりメインスレッド用
自分がドキュメント書くなら
NSMainQueueConcurrencyTypeはアプリケーションインターフェイスでの使用に特化しているため、アプリケーションのメインキューで利用されることを想定しています。
つまり、別スレッドで使わないでくれということ。
別スレッドで使うなとはどういうこと?
Appleのドキュメントから
プライベートキューによる並列処理の実現
ユーザに関係しないメインキューでデータ処理を実行することは、通常は避けてください。データ処理ではCPUリソースが大量に消費される可能性があり、メインキューで実行した場合は、ユーザインターフェイスが無反応になることもあり得ます。データをJSONからCore Dataにインポートするなど、アプリケーションでデータを処理する場合は、プライベートキューコンテキストを生成し、プライベートコンテキストでインポートを実行します。
多量のデータをCore Dataに保存するときメインスレッドでやると固まるから気をつけてくれよな!
- 多量のデータ?
- JSONでもなんでも、件数が固定ではないものは多量になりうると思っといたほうがいい
- ユーザはどんな操作をしているか分からない
- 動画撮ってるかもしれないし動画見てるかもしれないし
- そのときに同じメインスレッドで多量のデータをCore Dataに保存すると操作不能になる
- 再現性が低くて大変
保存用にプライベートキューのためのcontextを作る
MRではどうやって書き込み用のcontextを作っているか
+ (NSManagedObjectContext *) MR_newContext
{
return [self MR_context];
}
+ (NSManagedObjectContext *) MR_context
{
return [self MR_contextWithParent:[self MR_rootSavingContext]];
}
MR_contextWithParent
はすでに説明したとおりrootContextを親にして、新しいcontextをNSPrivateQueueConcurrencyType
で作成するだけ。すごくシンプルだ。
ここまでのまとめ
- マルチスレッドで書き込み用のContextは
rootSavingContext
を親にする-
NSPrivateQueueConcurrencyType
で書き込み用のcontextを作成する -
defaultContext
とは同じ親をもつ兄弟
-
- 何かを表示したい時、そのcontextは
defaultContext
なぜ親子関係にするの? Core Dataの基本について
- 子のcontextでsaveしたらそれを他のcontextにも反映したいから
例えば、
- コンテンツの一覧にお気に入りボタンがある
- 詳細画面でお気に入りボタンを押したらボタンの状態を一覧のボタンにも反映したい
親子関係で保存を伝えるとは?
- 書き込み時のcontextをsaveして親もsaveするとrootContextに変更が伝わって永続化される
- 親のcontextの変更は、子のcontextに反映される
- NSFetchedResultsControllerを使っていれば差分検出されdelegateのメソッドが動作
context親子の図
- 親: rootContext
- 子: 読み込み用 defaultContext
- ([順番2]
NSFetchedResultsController
で変更が検知できる)
- ([順番2]
- 子: 書き込み用 context
- ([順番1] このcontextで書き込んだら、context.parentでsaveすればrootに結果が伝わる)
- 子: 読み込み用 defaultContext
ここで疑問。なぜdefaultContext
をrootにしないんだろう?
- おそらく、書き込み用の
context
をsaveして親をsaveする処理自体は結構重い - おそらく、
defaultContext
の子を作って保存してdefaultContext
をsaveするのはメインスレッドの処理が多くなるのでは
defaultContext
の変更(話の優先度低い)
下記のメソッドでdefaultContext
を変更できる。
[NSManagedObjectContext MR_setDefaultContext:myNewContext];
しかし、変更できてもNSMainQueueConcurrencyType
なcontextを使うことが最も安全だし普通。MRのドキュメントでもrecommedはNSMainQueueConcurrencyType
なので、defaultを変更してカスタマイズなcontextを利用することはあまり無いと思う。
NOTE: It is highly recommended that the default context is created and set on the main thread using a managed object context with a concurrency type of NSMainQueueConcurrencyType.
書き込みcontextを使ってどういうふうな使い方をするの?
MRにはバックグラウンドスレッドで保存処理を行うメソッドが用意されている。
基本的にはデータの作成、保存は全てバックグラウンドスレッドでやれば良くて、特定のメソッドを使うようにすればいい。
Person *person = ...;
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Person *localPerson = [person MR_inContext:localContext];
localPerson.firstName = @"John";
localPerson.lastName = @"Appleseed";
}];
-
localContext
は保存用に作成される - MR_inContextで
localContext
から作成 - クロージャを終了するとときにはsaveされている
バックグラウンド保存のコードはどうなってる?
+ (void) saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
{
NSManagedObjectContext *savingContext = [NSManagedObjectContext MR_rootSavingContext];
NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:savingContext];
[localContext performBlock:^{
[localContext MR_setWorkingName:NSStringFromSelector(_cmd)];
if (block) {
block(localContext);
}
[localContext MR_saveWithOptions:MRSaveParentContexts completion:completion];
}];
}
-
rootSavingContext
から新しいlocalContext
を作成 - 引数として渡された
block
は、localContext performBlock:
に囲まれている
ここで気になるのは次の3つ
- performBlockメソッドとは?
- プライベートキューで作成されたContextで保存する際に利用する
-
MR_setWorkingName
とは? -
MR_saveWithOptions:
とは?
MR_setWorkingName
について見ていく
さっきのコードでは引数workingName
に対してNSStringFromSelector(_cmd)
でメソッド名を渡していて、実装は次のようになっている。
- (void)MR_setWorkingName:(NSString *)workingName
{
void (^setWorkingName)() = ^{
[[self userInfo] setObject:workingName forKey:MagicalRecordContextWorkingName];
};
if (self.concurrencyType == NSMainQueueConcurrencyType && [NSThread isMainThread])
{
setWorkingName();
}
else
{
[self performBlockAndWait:setWorkingName];
}
}
単に、userInfoにKey=MagicalRecordContextWorkingNameの ValueとしてStringを登録している。userInfoはNSMutableDictoinaryなので単に実行したメソッドを保持しているだけだろう。
なにそれ
Appleのリファレンスによると、The user infomation for the context
ということで、デバッグ用だろう。
次に MR_saveWithOptions
このメソッドはかなり長いので、先に引数 MRSaveParentContexts
について読んでおく。MRのOptionsだ。
typedef NS_OPTIONS(NSUInteger, MRSaveOptions) {
/** No options — used for cleanliness only */
MRSaveOptionNone = 0,
/** When saving, continue saving parent contexts until the changes are present in the persistent store */
// Google翻訳: 保存するときは、変更が永続ストアに存在するまで親コンテキストを保存し続けます。
MRSaveParentContexts = 1 << 1,
/** Perform saves synchronously, blocking execution on the current thread until the save is complete */
// Google翻訳: 同期を実行して、保存が完了するまで現在のスレッドで実行をブロックする
MRSaveSynchronously = 1 << 2,
/** Perform saves synchronously, blocking execution on the current thread until the save is complete; however, saves root context asynchronously */
// Google翻訳: 同期を実行し、保存が完了するまで現在のスレッドで実行をブロックします。ただし、ルートコンテキストを非同期で保存します
MRSaveSynchronouslyExceptRootContext = 1 << 3
};
MR_saveWithOptions
のユースケースとそれに対応するMRのメソッド
- MRSaveParentContextsを指定
- 非同期実行して親もsave
MR_saveToPersistentStoreWithCompletion
- MRSaveSynchronously
- 同期実行するので終わるまで待つ
MR_saveOnlySelfAndWait
- MRSaveParentContexts | MRSaveSynchronously
- バックグラウンドで同期実行して終わるまで待つ
- (void) MR_saveToPersistentStoreAndWait;
- MRSaveSynchronouslyExceptRootContext
- バックグラウンドスレッドで同期実行。
- rootContextはそれとは別スレッドの非同期実行
基本的にはMRSaveParentContext | MRSaveSynchronouslyで終わったらクロージャが動作するのが良さそう。
次に実際のMR_saveWithOptions
メソッドをコードの中にコメントを書きながら理解していく
//
- (void) MR_saveWithOptions:(MRSaveOptions)saveOptions completion:(MRSaveCompletionHandler)completion;
{
__block BOOL hasChanges = NO;
// NSConfinementConcurrencyTypeじゃないので次のifは通らない
if ([self concurrencyType] == NSConfinementConcurrencyType)
{
hasChanges = [self hasChanges];
}
else
{ // contextのperformBlockAndWaitを動作させてhasChangesがあるかどうかを取得(なぜ?)
[self performBlockAndWait:^{
hasChanges = [self hasChanges];
}];
}
if (!hasChanges)
{ // 何も変更無さそうならLog出力して
MRLogVerbose(@"NO CHANGES IN ** %@ ** CONTEXT - NOT SAVING", [self MR_workingName]);
// 完了用のクロージャをメインスレッドで実行する
if (completion)
{
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, nil);
});
}
return;
}
// 引数から動作を決める処理。Optionなので値を取り出す必要がある
// 次のshouldSaveParentContextsはtrueになる
BOOL shouldSaveParentContexts = ((saveOptions & MRSaveParentContexts) == MRSaveParentContexts);
// 次のshouldSaveSynchronouslyはfalse
BOOL shouldSaveSynchronously = ((saveOptions & MRSaveSynchronously) == MRSaveSynchronously);
// 次もfalse
BOOL shouldSaveSynchronouslyExceptRoot = ((saveOptions & MRSaveSynchronouslyExceptRootContext) == MRSaveSynchronouslyExceptRootContext);
// saveSynchronouslyは同期実行するかどうかで、falseになる
// optionが MRSaveSynchronouslyでMRSaveSynchronouslyExceptRootContextでない、
// もしくは
// optionが MRSaveSynchronouslyExceptRootContext で、このcontextがrootではない
BOOL saveSynchronously = (shouldSaveSynchronously && !shouldSaveSynchronouslyExceptRoot) ||
(shouldSaveSynchronouslyExceptRoot && (self != [[self class] MR_rootSavingContext]));
id saveBlock = ^{
MRLogInfo(@"→ Saving %@", [self MR_description]);
MRLogVerbose(@"→ Save Parents? %@", shouldSaveParentContexts ? @"YES" : @"NO");
MRLogVerbose(@"→ Save Synchronously? %@", saveSynchronously ? @"YES" : @"NO");
BOOL saveResult = NO;
NSError *error = nil;
@try
{ // まず保存
saveResult = [self save:&error];
}
@catch(NSException *exception)
{
MRLogError(@"Unable to perform save: %@", (id)[exception userInfo] ?: (id)[exception reason]);
}
@finally
{
[MagicalRecord handleErrors:error];
// 保存完了して、引数的にshouldSaveParentContextsもtrue、かつ親もいるならifに入る
if (saveResult && shouldSaveParentContexts && [self parentContext])
{
// Add/remove the synchronous save option from the mask if necessary
MRSaveOptions modifiedOptions = saveOptions;
if (saveSynchronously)
{ // 同期実行しないのでここは通らない
modifiedOptions |= MRSaveSynchronously;
}
else
{ // 非同期実行するから、optionからMRSaveSynchronouslyを消している
modifiedOptions &= ~MRSaveSynchronously;
}
// 親でsaveするため再帰処理
// If we're saving parent contexts, do so
[[self parentContext] MR_saveWithOptions:modifiedOptions completion:completion];
}
else
{ // 保存完了しなかったり、親で保存しなかったり、親がなかったりしたら終了(自分がroot)
if (saveResult)
{
MRLogVerbose(@"→ Finished saving: %@", [self MR_description]);
}
if (completion)
{
dispatch_async(dispatch_get_main_queue(), ^{
completion(saveResult, error);
});
}
}
}
};
if (saveSynchronously)
{
[self performBlockAndWait:saveBlock];
}
else
{
[self performBlock:saveBlock];
}
}
ポイントは
- 再帰処理で親contextをたどり、
rootContext
までsaveすれば終了 - hasChangesで変更をチェックしているが、
performBlockAndWait
でチェックする理由は知らん -
NSConfinementConcurrencyType
は非推奨になった何かなので無視
オプションにより同期処理もサポートした汎用的な書き込みメソッドのため手厚いが、やってることはそれほど大したことじゃないことがわかったと思う。
Magical Recordは使ったほうが良い?
- GitHubのリポジトリ見ると保守されていないと感じる
- 上のことをふまえたらSwiftで自分でやっていくほうがいい]
まとめ
- 電影少女2018はいいぞ
- ヴァイオレットエヴァーガーデンもいいぞ