16

私はリファクタリングする必要がある iPad 用の準備が整ったエンタープライズ (AppStore 以外の) レガシー iOS アプリケーションを持っています (それは別の開発者、私の現在の仕事の前任者によって書かれました)。

このアプリケーションは、MSSQL データベースを持つサーバーから JSON 経由でデータをフェッチします。データベース スキーマには約 30 のテーブルがあり、最も容量が大きいのは、それぞれ約 10.000 レコードを持つクライアント、市、代理店であり、将来的にはさらなる成長が予想されます。JSON が受信された後 (テーブルごとに 1 つの JSON 要求と応答のペア) - CoreData にマップされます - このプロセスには、対応する CoreData エンティティ (クライアント、都市、機関など) を互いに接着することも含まれます。 CoreData レイヤーでこれらのエンティティ間の関係を設定します。

それ自体、プロジェクトの CoreData fetch-part (または read-part) は大幅に最適化されています。CoreData が持つほとんどすべての可能なパフォーマンスとメモリ調整を使用していると思います。そのため、アプリケーションの UI レイヤーは非常に高速で応答性が高いため、その仕事は完全に満足のいくものであり、適切であると考えてください。


問題は、CoreData レイヤーの準備のプロセス、つまりサーバーからクライアントへの同期プロセスです。時間がかかりすぎます。30 個の JSON パック (「パック」とは「1 つのテーブル - 1 つの JSON」を意味します) になる 30 個のネットワーク リクエストを考えてみましょう。これらは 30 個の CoreData エンティティにマップされ、それらは互いに接着されます (適切な CoreData 関係がそれらの間に設定されます)。このプロジェクトでこれがどのように行われるか (遅すぎる) を最初に見たとき、頭に浮かんだ最初のアイデアは次のとおりでした。

「初めて完全な同期が実行されます (アプリの初回起動時) -たとえば、1 つのアーカイブ ファイル (データベース ダンプのようなもの) でデータベース データ全体のフェッチを実行し、それを全体としてコア データにインポートします。土地"。

しかし、そのような 1 ファイル ダンプの送信が可能であったとしても、CoreData では、対応する CoreData エンティティを接着して、それらの間に適切な関係を設定する必要があるため、それができるとは想像しがたいことに気付きました。このスキームに頼れば、パフォーマンスが向上します。

また、私の同僚は SQLite を Core Data の完全な代替手段として検討するよう提案してくれましたが、残念ながら私は SQLite を使用した経験がありません。同期プロセスが非常に遅いですが、私のアプリは動作します。特に UI パフォーマンスは非常に優れています)。SQLite について私が想像できる唯一のことは、Core Data とは対照的に、SQLite には古き良き外部キー システムがあるため、クライアント側でいくつかの追加の関係を接着する必要がないということですよね?


質問は次のとおりです (回答者の皆様、回答の際にこれらの点を混同しないでください。すべての質問について、私はあまりにも多くの混乱を抱えています)。

  1. 私が上で説明した方法で「データベース全体を初めて大量にインポートする」アプローチをとった経験のある人はいますか? JSON <-> CoreDataペアを利用するかどうかにかかわらず、解決策について知っていただければ幸いです。

  2. Core Data には、対応する 30 個のテーブル スキーマ (上記の "30 パックの JSON" 以外の特定のソースを使用する可能性があります) を大量に作成できるグローバル インポート メカニズムがありますか?

  3. 2)が不可能な場合、同期プロセスを高速化する可能性はありますか? ここで私が意味するのは、現在の JSON<->CoreData スキームの改善で、私のアプリが使用するものです。

  4. SQLite への移行: そのような移行を検討する必要がありますか? 私はそれから何を得ますか?では、複製→送信→クライアントの準備という全体のプロセスはどのように見えるでしょうか?

  5. CoreData と SQLite の他の代替手段 - それらはどのようなものか、またはどのように見えるでしょうか?

  6. 私が説明した状況について、他に何か考えやビジョンはありますか?


更新 1

Mundi によって書かれた回答は良いものですが (1 つの大きな JSON、SQLite を使用する場合は「いいえ」)、私が説明した問題について他の洞察があるかどうかはまだ興味があります。


更新 2

私の質問がそれを読むすべての人にとってかなり明確になることを期待して、私は自分の状況を説明するためにできる限り最善の方法でロシア語の英語を使用しようとしました. この 2 回目の更新までに、質問をさらに明確にするためのガイドをいくつか提供しようと思います。

2 つの二分法を検討してください。

  1. iOS クライアントのデータ レイヤーとして何を使用できますか?
  2. トランスポート層として何を使用できるか/使用すべきか-JSON(回答で提案されているように一度に1つのJSON、圧縮されている可能性もあります)またはいくつかのDB自体のダンプ(もちろん可能であれば-私がいることに注意してください私の質問でもこれを尋ねています)。

これら 2 つの二分法の交差によって形成される「セクター」は、最初のものから CoreData を選択し、2 番目のものから JSON を選択することは、iOS 開発の世界で最も広く普及しているデフォルトであり、私のアプリでも使用されています。この質問から。

そうは言っても、CoreData-JSONペアに関する回答と、他の「セクター」の使用を考慮した回答を見ていただければ幸いです(SQLiteとそのダンプアプローチのいくつかを選択するのはどうですか?)

また、重要なのは、現在のオプションを他のいくつかの代替手段にドロップするだけではなく、その使用の同期フェーズと UI フェーズの両方でソリューションを高速に動作させたいということです。したがって、現在のスキームの改善に関する回答と、他のスキームを示唆する回答は大歓迎です!

さて、私の現在の CoreData-JSON 状況の詳細を提供する次の更新 #3 を参照してください。


更新 3

前述したように、現在、私のアプリは 30 パックの JSON を受け取ります。つまり、テーブル全体に対して 1 パックです。たとえば、大容量のテーブルを見てみましょう: Client、Agency、City。

これは Core Data であるため、clientレコードに空でないagency_idフィールドがある場合、クラスの新しい Core Data エンティティを作成Agency (NSManagedObject subclass)し、このレコードの JSON データを入力する必要があります。そのため、このクラスのエージェンシーに対応する Core Data エンティティが既に必要です。Agency (NSManagedObject's subclass)、そして最後に、次のようなことをしてclient.agency = agency;から呼び出す必要があります[currentManagedObjectContext save:&error]。このようにして、後でこのクライアントにフェッチを要求し、その.agencyプロパティに対応するエンティティを見つけるように要求できます。このようにすると、完全に正気であることを願っています。

このパターンが次の状況に適用されると想像してください。

次の 3 つの個別の JSON パックを受け取りました: 10000 のクライアントと 4000 の都市と 6000 の機関 (クライアントには 1 つの都市があり、都市には多くのクライアントがあります。クライアントには代理店があり、代理店には多くのクライアントがあり、代理店には 1 つの都市があり、都市には多くの代理店があります)。

ここで、コア データ レベルで次の関係を設定したいと考えています。クライアント エンティティclientを対応する都市と対応する機関に接続する必要があります。

プロジェクトでのこれの現在の実装は非常に醜いことをします:

  1. 依存関係の順序は次のとおりです: City -> Agency -> Client つまり City を最初にベイクする必要があるため、アプリケーションは City のエンティティの作成を開始し、それらを Core Data に永続化します。

  2. 次に、機関の JSON を処理します。すべての JSON レコードを反復処理します。すべての機関について、新しいエンティティagencyを作成し、その によって、city_id対応するエンティティcityを取得し、agency.city = city. 機関の JSON 配列全体を反復処理した後、現在の管理オブジェクト コンテキストが保存されます (実際には、500 レコードが処理されるたびに -[managedObjectContext save:] が数回実行されます)。このステップでは、6000 の機関ごとにクライアントごとに 4000 の都市の 1 つを取得すると、同期プロセス全体のパフォーマンスに大きな影響があることは明らかです。

  3. 次に、最後にクライアントの JSON を処理します。前の 2 段階と同様に、10000 要素の JSON 配列全体を反復処理し、対応する機関と ZOMG 都市のフェッチを 1 つずつ実行します。これは、全体的なパフォーマンスに影響を与えます。前のステージ2と同じように。

それはすべて非常に悪いです。

ここで確認できる唯一のパフォーマンスの最適化は、最初の段階で、都市の ID (実際の ID の NSNumber を意味します) と失敗した City エンティティを値として含む大きな辞書を残すことができるため、次の醜い検索プロセスを防ぐことができることです。ステージ 2 で同様のキャッシング トリックを使用してステージ 3 で同じことを行いますが、問題は、[ Client-City、Client-Agency、Agency-City ] で説明した 30 個のテーブルすべての間にはるかに多くの関係があることです。すべてのエンティティのキ​​ャッシュを含む最終的な手順では、iPad デバイスがアプリ用に予約しているリソースを使用する可能性が最も高くなります。


更新 4

今後の回答者へのメッセージ: この回答が詳細で適切な形式になるように最善を尽くしましたが、詳細な回答を期待しています。あなたの答えが、ここで議論されている問題の複雑さに実際に対処し、私の質問を可能な限り明確かつ一般的にするために行った私の努力を補完してくれるなら、それは素晴らしいことです. ありがとう。

更新 5

関連トピック:サーバーからのデータをキャッシュするためのクライアント (iOS) 上の Core Data 戦略RestKit で POST 要求を作成し、応答を Core Data にマップしようとしています

更新 6

新しい報奨金を開くことができなくなり、回答が受け入れられた後でも、このトピックが扱う問題に関する追加情報を含む他の回答をお待ちしております. 前もって感謝します。

4

7 に答える 7

6

まず、あなたはたくさんの仕事をしていて、どのようにスライスしても時間がかかりますが、物事を改善する方法があります.

新しいオブジェクトを処理するためのバッチサイズと一致するバッチサイズで、フェッチをバッチで行うことをお勧めします。たとえば、新しいAgencyレコードを作成するときは、次のようにします。

  1. 現在のAgencyバッチが でソートされていることを確認してくださいcity_id。(理由は後で説明します)。

  2. バッチ内のCityそれぞれの ID を取得します。AgencyJSON がどのように構造化されているかにもよりますが、これはおそらく次のようなワンライナーです (valueForKey配列で動作するため):

    NSArray *cityIDs = [myAgencyBatch valueForKey:@"city_id"];
    
  3. City前のステップで見つけた ID を使用して、1 回のフェッチで現在のパスのすべてのインスタンスを取得します。結果を で並べ替えますcity_id。何かのようなもの:

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"City"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"city_id in %@", cityIDs];
    [request setPredicate:predicate];
    [request setSortDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"city_id" ascending:YES] ]];
    NSArray *cities = [context executeFetchRequest:request error:nil];
    

これで、 の1 つの配列Agencyと の別の配列がありCity、両方とも でソートされていますcity_id。それらを一致させて関係を設定します (city_id一致しない場合にチェックしてください)。変更を保存して、次のバッチに進みます。

これにより、必要なフェッチの数が劇的に減少し、速度が向上します。この手法の詳細については、Apple のドキュメントの効率的な検索または作成の実装を参照してください。

役立つかもしれないもう 1 つのことは、フェッチを開始する前に、必要なオブジェクトで Core Data の内部キャッシュを「ウォームアップ」することです。これにより、プロパティ値を取得するためにデータ ストアに移動する必要がないため、後で時間を節約できます。このためには、次のようにします。

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"City"];
// no predicate, get everything
[request setResultType:NSManagedObjectIDResultType];
NSArray *notUsed = [context executeFetchRequest:request error:nil];

..そして、結果を忘れてください。これは表面的には役に立ちませんが、City後でインスタンスへのアクセスを高速化するために、内部コア データの状態を変更します。

さて、その他のご質問ですが、

  • Core Data の代わりに SQLite を直接使用することは、あなたの状況にとってひどい選択ではないかもしれません。city_id利点は、フィールドを外部キーとして使用できるため、関係を設定する必要がないことです。というわけで早速インポート。もちろん、欠点は、モデル オブジェクトを SQL レコードとの間で変換する独自の作業を行う必要があり、コア データを前提とするかなり多くの既存のコードを書き直す必要があることです (たとえば、現在の関係に従うたびに、その外部キーでレコードを検索する必要があります)。この変更により、インポートのパフォーマンスの問題が解決される可能性がありますが、重大な副作用が生じる可能性があります。

  • データをテキストとして送信する場合、JSON は一般的に非常に優れた形式です。サーバー上に Core Data ストアを準備でき、そのファイルを既存のデータ ストアにマージするのではなく、そのまま使用することができれば、ほぼ確実に速度が向上します。インポート プロセスはサーバー上で 1 回実行され、その後は実行されません。しかし、それらは大きな「if」であり、特に 2 番目のものです。新しいサーバー データ ストアを既存のデータとマージする必要がある場合は、現在の場所に戻っています。

于 2013-07-30T21:26:02.817 に答える
5

サーバーを制御できますか? 次の段落からあなたがそうしているように聞こえるので、私は尋ねます:

「初めて完全な同期が実行されます (アプリの初回起動時) - たとえば、1 つのアーカイブ ファイル (データベース ダンプのようなもの) でデータベース データ全体のフェッチを実行し、何らかの方法でそれを全体として CoreData ランドにインポートします。 "。

ダンプを送信できる場合は、Core Data ファイル自体を送信してみませんか? コア データ (デフォルト) は SQLite データベースに支えられています。サーバー上でそのデータベースを生成し、圧縮してネットワーク経由で送信してみませんか?

これは、すべての JSON 解析、ネットワーク リクエストなどを排除し、単純なファイルのダウンロードとアーカイブの抽出に置き換えることができることを意味します。プロジェクトでこれを行ったところ、パフォーマンスが計り知れないほど改善されました。

于 2013-08-02T08:49:31.040 に答える
4

ここに私の推奨事項があります:

  • magicrecordを使用します。これは、多くの定型コードを節約する CoreData ラッパーであり、さらに非常に興味深い機能が付属しています。
  • 他の人が提案したように、すべての JSON を 1 つの要求でダウンロードします。最初の JSON ドキュメントをアプリに埋め込むことができれば、ダウンロード時間を節約し、アプリを初めて開いたときにデータベースへの入力を開始できます。また、magicalrecord を使用すると、この保存操作を別のスレッドで簡単に実行でき、すべてのコンテキストを自動的に同期できます。これにより、アプリの応答性が向上します。
  • 最初のインポートの問題を解決したら、その醜いメソッドをリファクタリングする必要があるようです。繰り返しますが、これらのエンティティを簡単に作成するには、magicalrecord を使用することをお勧めします。
于 2013-08-05T09:25:59.460 に答える
3

最近、かなり大規模なプロジェクトを Core Data から SQLite に移行しましたが、主な理由の 1 つは一括挿入のパフォーマンスでした。移行中に失われた機能がかなりありました。回避できる場合は、切り替えをお勧めしません。SQLite への移行後、Core Data が透過的に処理する一括挿入以外の領域で実際にパフォーマンスの問題が発生し、それらの新しい問題を修正したにもかかわらず、元に戻って実行するのにある程度の時間がかかりました. Core Data から SQLite への移行に時間と労力を費やしましたが、後悔があるとは言えません。

それが明確になったら、一括挿入のパフォーマンスを修正する前に、いくつかのベースライン測定値を取得することをお勧めします.

  1. 現在の状態でそれらのレコードを挿入するのにかかる時間を測定します。
  2. それらのオブジェクト間の関係の設定を完全にスキップしてから、挿入のパフォーマンスを測定します。
  3. 単純な SQLite データベースを作成し、それを使用して挿入パフォーマンスを測定します。これにより、実際の SQL 挿入を実行するのにかかる時間の非常に適切なベースライン見積もりが得られ、コア データのオーバーヘッドについても適切なアイデアが得られます。

挿入を高速化するためにすぐに試すことができるいくつかのこと:

  1. 一括挿入を実行するときは、アクティブなフェッチ済み結果コントローラーがないことを確認してください。アクティブとは、nil 以外のデリゲートを持つフェッチされた結果コントローラーを意味します。私の経験では、Core Data の変更追跡は、一括挿入を行う際に最もコストのかかる単一の操作でした。
  2. 単一のコンテキストですべての変更を実行し、この一括挿入が完了するまで、異なるコンテキストからの変更のマージを停止します。

内部で実際に何が起こっているかをより深く理解するには、Core Data SQL デバッグを有効にして、実行されている SQL クエリを確認します。理想的には、多くの INSERT といくつかの UPDATE を表示する必要があります。しかし、SELECT や UPDATE が多すぎる場合は、オブジェクトの読み取りや更新が多すぎることを示しています。

Core-Data プロファイラー インストゥルメントを使用して、Core Data で何が起こっているかについてより高レベルの概要を取得します。

于 2013-08-11T09:41:18.173 に答える