1. ホーム
  2. ios

[解決済み] iOS 5における高速かつ効率的なCoreデータインポートの実装

2022-12-17 22:15:32

質問

質問 : 子コンテキストに親コンテキストに保存された変更を表示させ、NSFetchedResultsControllerがUIを更新するきっかけにするにはどうしたらよいでしょうか?

以下はその設定です。

たくさんの XML データ (約 200 万のレコード、それぞれがテキストの通常の段落のサイズ) をダウンロードして追加するアプリがあります。.sqlite ファイルのサイズは約 500 MB になります。このコンテンツをCore Dataに追加するには時間がかかりますが、データをデータストアにインクリメンタルにロードしている間、ユーザーはアプリを使用できるようにしたいのです。大量のデータが移動していることをユーザーに気づかせず、ハングアップもせず、バターのようにスクロールする必要があります。しかし、アプリはデータを追加すればするほど便利になるので、Core Dataストアにデータが追加されるのをずっと待っているわけにはいきません。コードでは、これはインポート コードでこのようなコードを避けることを意味します。

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

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

以下は、私が現在のソリューションを開発するためにこれまでに使用したリソースです。

Apple の Core Data プログラミング ガイド。データを効率的にインポートする

  • 自動解放プールを使ってメモリを抑える
  • リレーションシップのコスト。フラットにインポートし、最後にリレーションシップのパッチを貼る
  • できればクエリしないでください。O(n^2)単位で処理速度が遅くなります。
  • バッチでのインポート: 保存、リセット、排出、および繰り返し
  • インポート時に Undo Manager をオフにする

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

  • 3 つのコンテキストを使用します。マスター、メイン、コンフィニメント コンテキスト タイプ

iDeveloper TV - Core Data for Mac, iPhone & iPad アップデート。

  • performBlockで他のキューに保存を実行すると、高速になります。
  • 暗号化すると遅くなるので、できればオフにしましょう。

Core Dataで大規模なデータセットをインポートして表示する by 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];

背景コンテキスト - このコンテキストは、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];
}

どのように解決するのですか?

マスター MOC もストライドで保存したほうがいいでしょう。 その MOC が保存するために最後まで待たされるのは意味がありません。 それは独自のスレッドを持っており、同様にメモリを抑えるのに役立ちます。

あなたは書きました。

そして、インポート処理の最後に、マスター/親コンテキストに保存します。 コンテキストに保存し、表向きは他の子コンテキストに変更をプッシュします。 コンテキストにプッシュします。

この構成では、2 つの子コンテキスト (メイン MOC とバックグラウンド MOC) があり、両方とも "master." の親コンテキストになっています。

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

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

あなたのシナリオでは、DidSave 通知中に MASTER 保存からマージすることによって、MAIN MOC にデータを引き入れています。

これは動作するはずで、どこで "hung." されているのか気になります。正規の方法でメイン 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つ目はそれ自身のperformanceBlockで十分に保護されていますが、2つ目はそうではありません(performanceBlockで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は保存を呼び出さなければならず、次にマスターは保存を呼び出さなければなりませんが、これらが行っているのはポインタの移動だけです...マスターがディスクに保存するまでは。

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

そこには いくつかの しかし、ディスクをヒットしたとき、すべての重い仕事はマスターで行われます。 そして、performBlock を使用してマスター上でこれらの保存をキックすると、メイン スレッドはリクエストを送信し、すぐに返します。

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