iOS 5での高速で効率的なコアデータインポートの実装


101

質問:NSFetchedResultsControllerをトリガーしてUIを更新するように、子コンテキストに親コンテキストで永続化された変更を表示するにはどうすればよいですか?

これが設定です:

大量のXMLデータ(約200万レコード、それぞれおよそテキストの通常の段落のサイズ)をダウンロードして追加するアプリがあります。.sqliteファイルのサイズは約500 MBになります。このコンテンツをCore Dataに追加するには時間がかかりますが、データが段階的にデータストアに読み込まれている間、ユーザーがアプリを使用できるようにする必要があります。大量のデータが移動されているので、ユーザーには見えないように見えないため、ハングやジッターが発生せず、バターのようにスクロールします。それでも、アプリの方が便利で、データが追加されるので、データがCore Dataストアに追加されるのをいつまでも待つことはできません。コードでは、これはインポートコードで次のようなコードを避けたいことを意味します。

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

アプリはiOS 5のみなので、サポートする必要がある最も遅いデバイスはiPhone 3GSです。

現在のソリューションを開発するためにこれまでに使用したリソースは次のとおりです。

Appleのコアデータプログラミングガイド:データの効率的なインポート

  • 自動解放プールを使用してメモリを維持する
  • 関係コスト。フラットにインポートし、最後に関係をパッチアップする
  • あなたがそれを助けることができるかどうか問い合わせないでください、それはO(n ^ 2)の方法で物事を遅くします
  • バッチでインポート:保存、リセット、排出、繰り返し
  • インポート時に元に戻すマネージャーをオフにする

iDeveloper TV-コアデータパフォーマンス

  • 3つのコンテキストを使用:マスター、メイン、制限コンテキストタイプ

iDeveloper TV-Mac、iPhone、iPadのコアデータの更新

  • performBlockを使用して他のキューで保存を実行すると、処理が速くなります。
  • 暗号化は物事を遅くし、可能であればそれをオフにします。

Marcus Zarraによるコアデータ内の大きなデータセットのインポートと表示

  • 現在の実行ループに時間を与えることでインポートを遅くすることができるので、ユーザーはスムーズに作業できます。
  • サンプルコードは、大規模なインポートを実行してUIの応答性を維持できることを証明していますが、3つのコンテキストや非同期のディスクへの保存ほど高速ではありません。

私の現在の解決策

NSManagedObjectContextのインスタンスが3つあります。

masterManagedObjectContext-これはNSPersistentStoreCoordinatorを持ち、ディスクへの保存を担当するコンテキストです。これを行うのは、保存が非同期になり、したがって非常に高速になるためです。起動時に次のように作成します。

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext-これは、UIがあらゆる場所で使用するコンテキストです。これは、masterManagedObjectContextの子です。私はそれを次のように作成します:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext-このコンテキストは、XMLデータをCore DataにインポートするNSOperationサブクラスで作成されます。オペレーションのメインメソッドで作成し、そこでマスターコンテキストにリンクします。

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

これは実際には非常に高速に動作します。この3つのコンテキスト設定を行うだけで、インポート速度を10倍以上向上させることができました。正直なところ、これは信じがたいことです。(この基本設計は、標準のコアデータテンプレートの一部である必要があります...)

インポートプロセス中に、2つの方法を保存します。バックグラウンドコンテキストで保存する1000アイテムごと:

BOOL saveSuccess = [backgroundContext save:&error];

次に、インポートプロセスの最後に、マスター/親コンテキストに保存します。これにより、表面的には、メインコンテキストを含む他の子コンテキストに変更がプッシュされます。

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

問題:問題は、ビューを再ロードするまでUIが更新されないことです。

NSFetchedResultsControllerを使用してデータが供給されているUITableViewを持つ単純なUIViewControllerがあります。インポートプロセスが完了すると、NSFetchedResultsControllerは親/マスターコンテキストからの変更がないので、見慣れたようにUIが自動的に更新されません。スタックからUIViewControllerをポップして再度ロードすると、すべてのデータがそこにあります。

質問:NSFetchedResultsControllerをトリガーしてUIを更新するように、子コンテキストに親コンテキストで永続化された変更を表示するにはどうすればよいですか?

私はアプリをハングさせるだけの以下を試しました:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}

26
これまでで最もよく準備され、最も準備された質問には+1000000。私も答えがあります...入力するのに数分かかります...
ジョディヘインズ

1
アプリがハングしていると言うと、どこにありますか?何してるの?
ジョディヘイギンズ、2012年

久しぶりに申し訳ありません。「フラットにインポートして、最後にリレーションシップをパッチアップする」とはどういう意味ですか?関係を確立するために、そのオブジェクトをメモリに保持する必要はありませんか?私はあなたのソリューションと非常によく似たソリューションを実装しようとしていますが、実際にいくつかの助けを借りてメモリフットプリントを下げることができます。
Andrea Sprega

この記事の最初の記事にリンクされているApple Docsを参照してください。これを説明します。幸運を!
David Weiss

1
本当に良い質問です。あなたが提供したセットアップの説明から、いくつかの巧妙なトリックを選びました
djskinner '11 / 03/28

回答:


47

おそらく、マスターMOCもストライドで保存する必要があります。保存するためにそのMOCが最後まで待つことは意味がありません。これには独自のスレッドがあり、メモリを節約するのにも役立ちます。

あなたが書いた:

次に、インポートプロセスの最後に、マスター/親コンテキストに保存します。これにより、表面的には、メインコンテキストを含む他の子コンテキストに変更がプッシュされます。

構成では、2つの子(メインMOCとバックグラウンドMOC)があり、どちらも「マスター」を親にします。

子を保存すると、変更が親にプッシュされます。そのMOCの他の子は、次にフェッチを実行するときにデータを参照します...明示的に通知されません。

したがって、BGが保存すると、そのデータはMASTERにプッシュされます。ただし、MASTERが保存するまで、このデータはディスク上にありません。さらに、MASTERがディスクに保存するまで、新しいアイテムは永続的なIDを取得しません。

シナリオでは、DidSave通知中にMASTER保存からマージすることにより、データをMAIN MOCにプルしています。

それはうまくいくはずなので、どこに「ぶら下がっている」のか知りたいです。メインのMOCスレッドで標準的な方法で実行されていないことに注意してください(少なくともiOS 5の場合は)。

また、おそらくマスターMOCからの変更をマージすることだけに関心があります(とにかく、登録はそれだけのように見えます)。update-on-did-save-notificationを使用する場合、これを行います...

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

さて、ハングに関するあなたの本当の問題かもしれないもののために...あなたはマスターに保存するために2つの異なる呼び出しを示します。最初のものはそれ自体のperformBlockで十分に保護されていますが、2番目のものはそうではありません(performBlockでsaveMasterContextを呼び出しているかもしれませんが...

ただし、このコードも変更します...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

ただし、MAINはMASTERの子であることに注意してください。したがって、変更をマージする必要はありません。代わりに、マスターでDidSaveを監視し、再フェッチするだけです!データはすでにあなたの親の中にあり、あなたがそれを要求するのを待っているだけです。これは、最初に親にデータがあることの利点の1つです。

検討すべきもう1つの代替案(そして私はあなたの結果について聞きたいと思います-それは大量のデータです)...

背景MOCをMASTERの子にする代わりに、MAINの子にします。

これを取れ。BGが保存するたびに、自動的にメインにプッシュされます。ここで、MAINはsaveを呼び出す必要があり、次にマスターはsaveを呼び出す必要がありますが、マスターがディスクに保存するまで、ポインターが移動しているだけです。

この方法の優れている点は、データがバックグラウンドのMOCからアプリケーションのMOCに直接送られることです(その後、通過して保存されます)。

パススルーにはいくらかのペナルティがありますが、ディスクに当たると、すべての重い作業がMASTERで行われます。そして、performBlockを使用してマスターでこれらの保存をキックすると、メインスレッドはリクエストを送信し、すぐに戻ります。

どうなるか教えてください!


すばらしい答えです。今日はこれらのアイデアを試して、発見したことを確認します。ありがとうございました!
David Weiss、

驚くばかり!それは完璧に機能しました!それでも、MASTER-> MAIN-> BGの提案を試してみて、そのパフォーマンスがどのように機能するかを確認します。これは非常に興味深いアイデアのようです。素晴らしいアイデアをありがとう!
David Weiss

4
performBlockAndWaitをperformBlockに変更するために更新されました。なぜこれが私のキューに再び現れたのかはわかりませんが、今回読んだとき、それは明らかでした...なぜ前にそれを手放したのかわかりません。はい、performBlockAndWaitは再入可能です。ただし、このようなネストされた環境では、親コンテキスト内から子コンテキストの同期バージョンを呼び出すことはできません。通知は(この場合は)親コンテキストから送信でき、デッドロックを引き起こす可能性があります。後でこれを読んでこれを読む人にはそれが明らかであることを願っています。ありがとう、David。
ジョディヘイギンズ

1
@DavidWeiss MASTER-> MAIN-> BGを試しましたか?このデザインパターンに興味があります。うまくいくかどうかを確認してください。ありがとうございました。
nonamelive 2012

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.