先日、potatotips #7 にて、Parse と上手に付き合うための tips を発表させていただきました。
この記事では、ParseSDK を使う上で気をつけていないとハマるポイントを、もう少し掘り下げつつ、発表には無かったものも含めてお送りしようと思います。
1. 非同期処理
1-1. **InBackground はメインスレッドをブロックする
名前からして、メインスレッドとは違うスレッドで非同期に処理をしてくれて、結果をコールバックにかえしてくれるように見えますが、メインスレッドをブロックするところがあるので Stop the World します。
内部的にはリクエストをキューに積んで逐次処理でさばいているようですが…
1-2. **InBackground のコールバックは Context の生死にかかわらず呼ばれる
よくあるカンタンな実装として、以下の様なものがあると思います。
public class SomeActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ParseQuery<ParseObject> query = ParseQuery.getQuery("SomeData");
query.findInBackground(new FindCallback<ParseObject>() {
@Override
public void done(List<ParseObject> result, ParseException e) {
}
});
}
}
Activity が死んだら、コールバックは呼ばれてほしくないですが、Activity の死活管理をしていないので、コールバックが返ってきます。
内部的には、Bolts フレームワークと同じものが使われています。
1-3. 同期呼び出し API は別のスレッドでリクエストを投げている
リクエストを同期的に実行する API も、実装としてはリクエストの実行は別のスレッドでやっています。
つまり、同期呼び出し API といいつつ、単に別のスレッドの結果を待つだけのことしかしていません。
同期呼び出し API を実行しているスレッドに割り込み等が発生すると、待っているところがInterruptedException
を吐き出します。
SDK では、このInterrutedException
をキャッチして、RuntimeException
に翻訳してスローするようになっています。
たとえば、AbstractThreadedSyncAdapter
を使ってデータを同期するようなことを考えると、同期のキャンセルが発生した時に、AbstractThreadedSyncAdapter
のスレッドが途中で終わらせられるので、上記のような状況が発生します。
1-4. リトライ処理でクラッシュする
わりと古い Apache HttpClient を使用しているようです。リクエストのリトライで、SingleClientConnManager
がうまく使えずIllegalStateException
となる事があり、クラッシュの原因となります。
1-5. エラーコード表にないコードのエラーが帰ってくることがある
Parse も知り得ない謎のコードもあるようです。
2. オブジェクトの操作
2-1. レスポンスデータの取得の都度ロック取得があり、かつデータの存在確認をしている。
例えば、キー名を指定してデータを取得する際には、以下の様なコードを書きます。
ParseObject object = // ...
String name = object.getString("name");
ParseObject#get**(String)
のメソッド名からして、オンメモリのデータをすぐに返してくれそうなイメージが有りますが、並列性のため、アクセスの度にロックを取得します。
ロックの取得の上で、メモリにデータが有るかどうかも確認しますが、この確認にもロックの取得が行われます。
ロックのコストが嵩んでいるため、そこそこの時間を要することになります。
2-2. データ構造以上に様々な情報を持つのでメモリを割りとよく食う
ParseObject
は内部的に、単なるデータを保持するMap
構造以外にも、どのような操作を実行したかを記録するキューがあったり、必要なTask
を保持するキューがあったりと、デコンパイルすると本当にいろいろなデータを保持していることがわかります。
ParseObject
を拡張してデータ構造とモデルの兼用なクラスを以下のように作ると、サーバから取得したデータやその他のデータ構造の上に個々のデータのフィールドも持つことになるので、メモリをバクバク食います。
public void MyData extends ParseObject implements Parcelable {
private String mObjectId;
private String mName;
private Date mCreatedAt;
private Date mUpdatedAt;
// ...
}
仮に、ParseObject
を拡張せず、自分でデータ構造の型(ex. SomeData
)を定義した場合、List<ParseObject>
からList<SomeData>
にマップする処理を走らせると、結構な回数の GC が走っているのが見て取れると思います。