12

私は数週間取り組んできた問題を抱えています。Core Data マネージド オブジェクト コンテキストを保存するたびに、UI のパフォーマンスが低下します。私は自分でできる限りのことをしてきたので、助けを求めています。

状況

私のアプリケーションは 2 つのNSManagedObjectContextインスタンスを使用します。1 つはアプリケーション デリゲートに属し、永続ストア コーディネーターがアタッチされています。Classもう 1 つはメイン MOC の子で、 と呼ばれるオブジェクトに属しますPhotoFetcherNSPrivateQueueConcurrencyTypeこの MOC で実行されるすべての操作は、バックグラウンド キューで行われるように使用されます。

このアプリケーションは、API から写真に関するデータを表す JSON データをダウンロードします。API からデータを取得するために、次の一連の手順が実行されます。

  1. NSURLRequestオブジェクトを構築し、プロトコルを使用してNSURLConnectionDataDelegateリクエストから返されたデータを構築するか、エラーを処理します。
  2. JSON データのダウンロードが完了したら、次のことを行うセカンダリ MOC のキューでブロックを実行します。
    1. NSJSONSerializationFoundation クラスのインスタンスを使用して JSON を解析します。
    2. 解析されたデータを反復処理し、必要に応じてバックグラウンド コンテキストでエンティティを挿入または更新します。通常、これにより、約 300 の新しいエンティティまたは更新されたエンティティが生成されます。
    3. バックグラウンド コンテキストを保存します。これにより、私の変更がメインの MOC に反映されます。
    4. メイン MOC でブロックを実行して、そのコンテキストを保存ます。これは、データをSQLiteストアであるディスクに永続化するためです。最後に、応答が Core Data ストアに完全に挿入されたことを通知するデリゲートへのコールバックを行います。

バックグラウンド MOC を保存するコードは次のようになります。

[AppDelegate.managedObjectContext performBlock:^{
    [AppDelegate saveContext]; //A standard save: call to the main MOC
}];

メイン オブジェクト コンテキストが保存されると、メイン オブジェクト コンテキストが最後に保存されてからダウンロードされたかなりの数の JPEG も保存されます。現在、iPhone 4 では、200x200 の JPEG を 15 個、圧縮率 70% で、合計で約 2MB のデータをダウンロードしています。

問題

これは機能し、うまく機能します。私の問題は、バックグラウンド コンテキストが保存NSFetchedResultsControllerされると、View Controller での実行がメイン MOC に伝達された変更を取得することです。PSTCollectionViewのオープンソースクローンである に新しいセルを挿入しますUICollectionView。新しいセルを挿入している間、メイン コンテキストはそれらの変更を保存してディスクに書き込みます。これは、iOS 5.1 を実行している iPhone 4 では、250 ~ 350 ミリ秒かかります。

その 3 分の 1 の間、アプリは完全に応答しません。保存前に進行中だったアニメーションは一時停止され、保存が完了するまで新しいユーザー イベントはメインの実行ループに送信されません。

Time Profiler を使用して Instruments でアプリを実行し、メイン スレッドをブロックしているものを特定しました。残念ながら、結果はかなり不透明です。これは、Instruments から得た最も重いスタック トレースです。

最も重いスタック トレースの計測

永続的なストアに更新を保存しているように見えましたが、確信が持てませんでした。そのため、MOC がディスクに触れないように呼び出しをまったく削除しsaveContextましたが、メイン スレッドでのブロッキング呼び出しはまだ存続しています。

テキスト形式のトレースは次のようになります。

Symbol Name
-[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
 -[NSManagedObjectContext executeFetchRequest:error:]
  -[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]
   _perform
    _dispatch_barrier_sync_f_invoke
     _dispatch_client_callout
      __82-[NSManagedObjectContext(_NestedContextSupport) executeRequest:withContext:error:]_block_invoke_0
       -[NSManagedObjectContext(_NestedContextSupport) _parentObjectsForFetchRequest:inContext:error:]
        -[NSManagedObjectContext executeFetchRequest:error:]
         -[NSPersistentStoreCoordinator executeRequest:withContext:error:]
          -[NSSQLCore executeRequest:withContext:error:]
           -[NSSQLCore objectsForFetchRequest:inContext:]
            -[NSSQLCore newRowsForFetchPlan:]
             -[NSSQLCore _newRowsForFetchPlan:selectedBy:withArgument:]
              -[NSSQLiteConnection execute]

私が試したこと

Core Data コードに触れる前に、最初に行ったのは JPEG の最適化です。小さい JPEG に切り替えたところ、パフォーマンスが向上しました。次に、一度にダウンロードする JPEG の数を減らしました (90 から 15 に減らしました)。これは、パフォーマンスの大幅な向上にもつながります。ただし、メイン スレッドで 250 ~ 350 ミリ秒の長さのブロックがまだ見られます。

私が最初に試みたのは、バックグラウンドの MOC を取り除き、それが問題を引き起こしている可能性を排除することでした。実際、更新または作成コードがメイン スレッドで実行されていたため、アニメーション全体のパフォーマンスが低下していたため、事態はさらに悪化しました。

永続ストアを に変更しNSInMemoryStoreTypeても効果はありませんでした。

バックグラウンドで管理されたオブジェクト コンテキストが約束した UI パフォーマンスを実現する「秘密のソース」を誰か教えてもらえますか?

4

2 に答える 2

6

いくつかの仮定を立てますが、あなたの説明からすると、それは妥当だと思います。

まず、メインの MOC が次のように作成されていると仮定します。

[[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

私がこの仮説を立てる理由は...

  1. 親コンテキストとして使用するため、 または のいずれかである必要がありNSMainQueueConcurrencyTypeますNSPrivateQueueConcurrencyType

  2. 他の場所で使用するため、メイン キューでアクセスできることが保証されている場合にのみ使用します。

ここで、メイン MOC を別の親 MOC ではなく、永続ストア コーディネーターに直接接続していると仮定します。

バックグラウンド MOC が保存を行うと、その変更はメイン MOC に反映されます (まだ保存されていません)。FRC はメイン MOC に接続されているため、これらの変更はすぐに表示されます。

メイン MOC で保存を発行すると、保存が完了するまで保存が返されないため、コードがブロックされます。

したがって、あなたが見ているものは完全に期待されています。

問題を解決するための多くのオプションがあります。

私の最初の提案は、プライベート キュー MOC を作成し、それをメイン キュー MOC の親にすることです。

これは、メイン キュー MOC の保存がブロックされないことを意味します。代わりに、データを親に「保存」します。親は、実際のデータベースを独自のプライベート キューに保存し、その結果、別のスレッドでバックグラウンドで保存します。

これで、メインスレッドのブロックの問題が解決されます。このメカニズムは、データベースをロードする子バックグラウンド MOC にも適しています。

iOS 5 にはネストされたコンテキストに関連するバグがいくつかありますが、iOS 6 をターゲットにしている場合、それらのほとんどは修正されていることに注意してください。

詳細については、「 Core Data could not fullfil for object after acquirePermanantIDs 」を参照してください。

編集

あなたの仮定は正しいですが、私は iOS 5 をターゲットにしており、iOS 6 のメイン MOC に対する親 MOC しか持つことができません (FRC でデッドロックが発生します)。– アッシュ・フロウ

dispatch_syncデッドロックが発生している場合は、最初にandperformBlockAndWait呼び出しを取り除きます。メインスレッド内からのブロッキング操作の使用は、最も単純な同期 (つまり、データ構造からの同期読み取り操作) 以外では決して行われるべきではなく、必要な場合にのみ行われます。また、同期呼び出しは予期しないデッドロックを引き起こす可能性があるため、特に直接制御しないコードを呼び出す場合は、可能な限り回避する必要があります。

それができない場合は、他にもいくつかのオプションがあります。私が最も気に入っているのは、FRC をプライベート キュー MOC にアタッチし、メイン スレッドを介して FRC への/からのアクセスを「接着」することです。dispatch_asyncテーブルビュー(またはその他のもの)へのデリゲート更新を処理するために、メインスレッドにアクセスできます。例えば...

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    dispatch_async(dispatch_get_main_queue(), ^{
        [tableView beginUpdates];
    });
}

アクセスが適切に同期されていることを確認するための単なるフロントエンドである FRC のプロキシを作成できます。完全なプロキシである必要はありません... FRC を使用する目的と、すべての操作が適切に同期されるようにするだけで十分です。

実際には、思ったより簡単です。

実際には、「メイン MOC」がメイン キュー MOC ではなくプライベート キュー MOC になることを除いて、現在とまったく同じ設定になります。したがって、データベース アクセスが発生しても、メイン スレッドはブロックされません。

ただし、適切なコンテキストで FRC を使用するには、いくつかの予防措置を講じる必要があります。

もう 1 つのオプションは、データベースへの変更を処理するために現在行っているように、メイン MOC と FRC を使用することですが、データベースのすべての変更は、永続ストア コーディネーターに直接接続された個別の MOC を経由するようにします。これにより、別のスレッドで変更が行われます。次に、MOC 保存通知を使用して、コンテキストを更新したり、ストアからデータを再取得したりします。

iOS と OSX の更新により、Core Data のネストされたコンテキストに関する多くの問題が修正されますが、以前のバージョンをサポートする際に心配しなければならないことがいくつかあります。

于 2012-10-29T01:15:07.723 に答える
0

I have a few guesses, in the order in which you should check them:

  1. It looks an awful lot like whatever is being slow is happening after the data is saved (i.e., it's in a callback). This leads me to it being either the reload of the PTSCollectionView or the re-fetch of the fetched results controller.

  2. Does this issue happen with UICollectionView on iOS 6.x? If not, that would lead me lean on PTSCollectionView.

  3. If it still happens, then that means it's likely not the collection view, but instead the fetched results controller. It sort of looks like, from the stack frames (opaque though they may be) that the fetched results controller is trying to perform a fetch which happens through a dispatch_barrier. Those are used to ensure a block isn't executed until after the barrier is reached. I'm out on a limb here, but you might want to check to see this is because internally, Core Data is saving elsewhere, and is thus delaying the execution of any other fetch requests. Again, that's kind of a wild and uneducated guess. But I'd try without the fetched results controller updating right away and see if your stutter still happens.

Another thing which stands out to me is you are performing a lot of work on a child MOC but then performing the save on the parent. It seems like the bulk of the save should be performed by the child. But it could also be I haven't worked with this part of Core Data in a while :-)

于 2012-10-28T17:06:49.660 に答える