web-dev-qa-db-ja.com

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

質問: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-Core Data Performance

  • 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データをコアデータにインポートする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];
}
97
David Weiss

おそらく、マスター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にプッシュされます。さて、MAINはsaveを呼び出さなければならず、その後マスターはsaveを呼び出さなければなりませんが、それらはすべて、マスターがディスクに保存されるまでポインターを移動するだけです。

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

パススルーにはsomeのペナルティがありますが、ディスクにヒットすると、すべての重いリフティングがMASTERで実行されます。そして、performBlockを使用してこれらのセーブをマスターでキックすると、メインスレッドはリクエストを送信し、すぐに戻ります。

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

47
Jody Hagins