7

私は初めての iOS アプリを作成しています。理論的には非常に簡単なはずですが、App Store に自信を持って提出できるほど十分な防弾を実現するのに苦労しています。

簡単に言うと、メイン画面にはテーブル ビューがあり、行を選択すると、選択した行に関連する情報をマスター/詳細形式で表示する別のテーブル ビューに切り替わります。基になるデータは、Web サービスから 1 日に 1 回 JSON データとして取得され、コア データ ストアにキャッシュされます。その日より前のデータは削除され、SQLite データベース ファイルが無限に大きくなるのを防ぎます。すべてのデータ永続化操作は、Core Data を使用して実行されNSFetchedResultsController、詳細テーブル ビューを支えます。

私が見ている問題は、新しいデータが取得、解析、および保存されている間にマスター画面と詳細画面を数回すばやく切り替えると、アプリがフリーズまたは完全にクラッシュすることです。メインスレッドがフェッチを実行しようとしている間にコアデータがバックグラウンドでデータをインポートしているために、ある種の競合状態があるようですが、私は推測しています. 意味のあるクラッシュ情報を取得するのに苦労しました。通常、それは Core Data スタックの奥深くにある SIGSEGV です。

以下の表は、詳細テーブル ビュー コントローラーが読み込まれたときに発生するイベントの実際の順序を示しています。

メイン スレッド バックグラウンド スレッド
viewDidLoad

                                     JSON データを取得する (AFNetworking を使用)

子 NSManagedObjectContext (MOC) を作成する

                                     JSON データを解析する
                                     子 MOC に管理対象オブジェクトを挿入する
                                     子 MOC を保存
                                     インポート完了通知の投稿

インポート完了通知を受け取る
親MOCを保存
フェッチを実行してテーブル ビューをリロードする

                                     子 MOC の古い管理対象オブジェクトを削除する
                                     子 MOC を保存
                                     投稿削除完了通知

削除完了通知を受け取る
親MOCを保存

JSON データが到着したときに AFNetworking 完了ブロックがトリガーされると、入れ子NSManagedObjectContextが作成され、JSON データを解析してオブジェクトをコア データ ストアに保存する「インポーター」オブジェクトに渡されます。インポーターはperformBlock、iOS 5 で導入された新しいメソッドを使用して実行されます。

NSManagedObjectContext *child = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [child setParentContext:self.managedObjectContext];        
    [child performBlock:^{
        // Create importer instance, passing it the child MOC...
    }];

インポーター オブジェクトは、独自の MOC を監視しNSManagedObjectContextDidSaveNotification、詳細テーブル ビュー コントローラーによって監視される独自の通知を投稿します。この通知が送信されると、Table View Controller は独自の (親) MOC で保存を実行します。

その日の新しいデータがインポートされた後に古いデータを削除するために、「deleter」オブジェクトで同じ基本パターンを使用します。これは、フェッチされた結果コントローラーによって新しいデータがフェッチされ、詳細テーブル ビューが再ロードされた後、非同期に発生します。

私が行っていないことの 1 つは、マージ通知を観察したり、管理対象オブジェクト コンテキストまたは永続ストア コーディネーターをロックしたりすることです。これは私がすべきことですか?これをすべて正しく設計する方法が少しわからないので、アドバイスをいただければ幸いです。

4

4 に答える 4

3

iOS 5 より前のバージョンでは、通常 2 つNSManagedObjectContextsありました。1 つはメイン スレッド用、もう 1 つはバックグラウンド スレッド用です。バックグラウンド スレッドは、データをロードまたは削除してから保存できます。結果NSManagedObjectContextDidSaveNotificationは(あなたがやっているように)メインスレッドに渡されました。mergeChangesFromManagedObjectContextDidSaveNotification:これらをメインスレッドのコンテキストに持ち込むために呼び出しました。これは私たちにとってうまくいきました。

これの重要な側面の 1 つは、メイン スレッドでの実行が終了するまでsave:バックグラウンド スレッドがブロックmergeChangesFromManagedObjectContextDidSaveNotification:されることです (リスナーからその通知に対して mergeChanges... を呼び出すため)。これにより、メイン スレッドの管理対象オブジェクト コンテキストがこれらの変更を認識できるようになります。親子関係がある場合に必要かどうかはわかりませんが、旧機種ではいろいろトラブルを避けるためにそうしていました。

2 つのコンテキスト間に親子関係があることの利点が何であるかはわかりません。あなたの説明から、ディスクへの最終的な保存はメインスレッドで行われるようですが、これはおそらくパフォーマンス上の理由から理想的ではありません。(特に、大量のデータを削除する可能性がある場合。アプリでの削除の主なコストは、ディスクへの最終的な保存時に常に発生しています。)

コアデータの問題を引き起こしている可能性のあるコントローラーが表示/非表示になったときに実行しているコードは何ですか? どのような種類のスタック トレースでクラッシュが見られますか?

于 2012-07-04T20:56:02.673 に答える
2

マルチスレッドのコア データで私が抱えていた主な問題は、作成されたスレッド/キュー以外のスレッド/キュー内のマネージド オブジェクトに誤ってアクセスすることです。

NSAssert を追加して、メインのマネージド オブジェクト コンテキストで作成されたマネージド オブジェクトがそこでのみ使用され、バックグラウンド コンテキストで作成されたものがメイン コンテキストで使用されないことを確認するのが良いデバッグ ツールであることがわかりました。

これには、NSManagedObjectContext と NSManagedObject のサブクラス化が含まれます。

  • iVar を MOC サブクラスに追加し、iVar が作成されたキューに割り当てます。
  • MO サブクラスは、現在のキューがその MOC のキュー プロパティと同じであることを確認する必要があります。

ほんの数行のコードですが、長い目で見れば追跡が困難なエラーを防ぐことができます。

于 2012-07-04T21:12:00.183 に答える
2

NSFetchedResultsController大量の削除に少し敏感であることが証明されているので、最初に掘り下げ始めます。

私の最初の質問は、テーブルビューの再フェッチとリロードが削除操作の開始にどのように関連しているかということです。NSFetchedResultsControllerまだフェッチしている間に、削除ブロックが子 MOC を保存する可能性はありますか?

詳細ビューからマスターに切り替えてから詳細ビューに戻ると、複数の同時バックグラウンド タスクが実行される可能性はありますか? それとも、特定の行に関連するデータだけでなく、Web サービスからすべてのデータを一度に取得していますか?

これをより堅牢にするための 1 つの代替方法は、以下を使用するのと同様のパターンを使用することUIManagedDocumentです。

親 MOC をメイン スレッド同時実行タイプとして使用する代わりに、UIManagedDocument実際にはメイン MOC をプライベート キューとして作成し、子 MOC をメイン スレッドで使用できるようにします。ここでの利点は、すべての I/O がバックグラウンドで実行され、親 MOC への保存が子 MOC に明示的に通知されるまで、子 MOC にまったく干渉しないことです。これは、 save commit が子から親への変更をコミットするためであり、その逆ではないためです。

したがって、プライベートな親キューで削除を行った場合、それはNSFetchedResultsControllerスコープにまったく含まれません。これは古いデータであるため、実際にはこれが推奨される方法です。

私が提供する別の方法は、次の 3 つのコンテキストを使用することです。

主なMOC ( NSPrivateQueueConcurrencyType)

  • 古いデータの永続的な保存と削除を担当します。

子MOC A ( NSMainQueueConcurrencyType)

  • UI関連とNSFetchedResultsControllerを担当

子 MOC B ( NSPrivateQueueConcurrencyType、子 MOC A の子)

  • 新しいデータを挿入し、完了したらそれを子 MOC A にコミットする責任があります。
于 2012-07-05T06:17:15.277 に答える
2

単なるアーキテクチャのアイデア:

あなたが述べたデータ更新パターン(1日1回、データの完全なサイクルの削除と追加)を使用すると、実際には毎日新しい永続ストアを作成し(つまり、カレンダーの日付に基づいて名前を付けます)、完了通知で、テーブルビューは、新しいストア (およびおそらく新しい MOC) に関連付けられた新しい fetchedresultscontroller をセットアップし、それを使用して更新します。その後、アプリは (別の場所で、おそらくその通知によってトリガーされる) 「古い」データ ストアを完全に破棄できます。この手法は、アプリが現在使用しているデータ ストアから更新処理を分離し、新しいデータへの "切り替え" は劇的にアトミックであると見なされる可能性があります。、変更が発生するため、新しいデータが書き込まれている間 (ただし、まだ完了していない)、一貫性のない状態でストアをキャッチしないことを期待するのではなく、新しいデータを指し始めているだけです。

明らかに、いくつかの詳細を省略しましたが、使用中に変更される多くのデータを再構築して、発生している種類のクラッシュの可能性を減らす必要があると考える傾向があります。

さらに議論していただければ幸いです...

于 2012-07-04T20:51:01.120 に答える