6
5

More than 5 years have passed since last update.

Core Dataのリファレンスにある使い方を知るためにMagical Recordのライブラリを読んでみる

Last updated at Posted at 2018-02-21
1 / 47

このスライドは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の難問その一、「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で変更が検知できる)
    • 子: 書き込み用 context
      • ([順番1] このcontextで書き込んだら、context.parentでsaveすればrootに結果が伝わる)

ここで疑問。なぜ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は使ったほうが良い?


まとめ

  • 電影少女2018はいいぞ
  • ヴァイオレットエヴァーガーデンもいいぞ
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5