101

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

セットアップは次のとおりです。

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

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

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

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

Apple の Core Data Programming Guide: Efficiently Importing Data

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

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

  • 3 つのコンテキストを使用: マスター、メイン、および監禁コンテキスト タイプ

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

  • performBlock を使用して他のキューで保存を実行すると、処理が速くなります。
  • 暗号化は速度を低下させます。可能であればオフにしてください。

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

  • 現在の実行ループに時間を与えることでインポートを遅くすることができるため、ユーザーは物事をスムーズに感じることができます。
  • サンプル コードは、大規模なインポートを実行して 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 倍以上向上しました。正直なところ、これは信じがたいことです。(この基本設計は、標準の Core Data テンプレートの一部である必要があります...)

インポート プロセス中に、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];
}
4

1 に答える 1

47

おそらく、マスター MOC も段階的に保存する必要があります。そのMOCが最後まで保存するのを待つ意味はありません。独自のスレッドがあり、メモリを抑えるのにも役立ちます。

あなたが書いた:

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

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

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

そのため、BG が保存すると、そのデータが 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つの異なる呼び出しを示しています。1 つ目は独自の 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 つです。

検討すべき別の選択肢(そして、あなたの結果について聞きたいです-それは多くのデータです)...

バックグラウンド MOC を MASTER の子にする代わりに、MAIN の子にします。

これを取れ。BG が保存されるたびに、自動的に MAIN にプッシュされます。ここで、MAIN が save を呼び出さなければならず、次に master が save を呼び出さなければなりませんが、それらはポインターを移動するだけです... マスターがディスクに保存するまで。

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

パススルーには多少のペナルティがありますが、マスターがディスクにヒットすると、すべての重い作業がマスターで行われます。そして、performBlock を使用してマスターでこれらの保存を開始すると、メイン スレッドは要求を送信するだけで、すぐに戻ります。

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

于 2012-05-11T02:37:11.527 に答える