2012/07/05

Android:高速化。ContentResolver?ContentProviderClient?


ContentProviderClientというクラスがあります。
これはContentResolver同様、ContentProviderとやり取りするためのクライアントです。

アプリからこれを使用するにはContentResolverのacquireContentProviderClient(Uri)
またはacquireContentProviderClient(String)を呼び出してインスタンスを取得すします。

ContentProviderClientのAPIを見るとContentResolverと似ていることがわかります。
ContentProviderClient:Android Developers


●ContentResolverとContentProviderClientの違いは何か?

ContentResolverはDBへ問合せる時、毎回コンテンツURIのAUTHORITYをキーに
ContentProviderを検索します。
そして、問合せ結果のcursorがcloseされるとContentProviderへの参照を破棄します。

ContentProviderClientは検索して取得したContentProviderへの参照を保持し続けます。
これは、問合せ結果のcursorがcloseされても続きます。
ContentProviderClientは、ContentResolverがクエリ発行の度にContentProviderを検索
する処理を省略する分高速に動作します。

また、ContentResolverと異なり、ContentProviderClientはqueryとopenFileメソッドは
スレッドセーフではありません。


●ContentProviderClientを扱う場合の注意点

開発者はContentProviderClientが不要となった場合にこれを明示的に破棄(release() )
する必要があります。
これを怠ると、システムがContentProviderの必要・不要を判断できないためリークに繋
がります。


●ContentProviderプロセスの死亡とクライアントプロセスの死亡

ContentResolver経由でCursorオブジェクトを取得した場合、Cursor取得~Cursor.close()
までの間に対象のContentProviderプロセスが死亡すると、呼び出し側のプロセスも道連れ
となります。

例えば、あなたのアプリが電話帳データを取得している最中にacoreプロセスが死亡すると、
あなたのアプリプロセスも強制終了されます。
これは、ContentProviderプロセスが死亡した時、これに依存するプロセスも強制終了させ
るようシステムがクリーンアップする仕組みです。

ContentProviderClientの場合は、ContentProviderClientの取得~破棄(release())まで
となります。

ContentProviderClientはその性質上、キャッシュされる期間がonResume~onPauseに相当
する程長くなりがちです。
ContentResolverの強制終了され得る範囲がCursor取得~Cursor.close()であることを
考えると、ContentProviderClientが強制終了され得る範囲は非常に長くなります。
※これについてはJellyBean以降、関連するAPIが追加されました


●それぞれのメリット・デメリット

ContentProviderClientはContentResolverと比べて高速に動作するメリットがあります。

軽量な電話帳データ1件をqueryで取得した場合のパフォーマンスは下記です。
平均:125ms (ContentResolver)
平均:23ms  (ContentProviderClient)
ContentProviderClientの方が5倍以上高速です。
# 両者の差はContentProviderの検索時間がほとんどなので、1度に大量のデータを取得す
# るような場合、この恩恵はあまり感じられないかもしれません。

ただし、ContentProviderClientは明示的に破棄(release())する必要があるため、
別途管理しないといけないデメリットがあります。
また、ContentProviderの強制終了による"道連れ強制終了"を少なからず考慮する必要が
あるでしょう。
※これについてはJellyBean以降、関連するAPIが追加されました

もし、あなたのアプリがContentResolver経由で軽量なクエリを複数回呼び出しているよ
うであれば、ContentProviderClientに置き換えることで大きなパフォーマンス向上が見込
めます。
ただし、ContentProviderClient(release())を管理することを忘れてはいけません。


●サンプルコード

ContentProviderClientを取得するacquireContentProviderClientメソッドの引数には、
対象のContentProviderを取得できるURIを指定します。
@Override
protected void onResume() {
    super.onResume();
    initContentProviderClient();
}

@Override
protected void onPause() {
    super.onPause();
    releaseContentProviderClient();
}

private void initContentProviderClient() {
    releaseContentProviderClient();
    mContentProviderClient = getContentResolver()
            .acquireContentProviderClient(
                    ContactsContract.Contacts.CONTENT_URI);
}

private void releaseContentProviderClient() {
    if (mContentProviderClient != null) {
        mContentProviderClient.release();
    }
}

private void onUpdate() {
    Cursor c = null;
    try {
        c = mContentProviderClient.query(
                ContactsContract.Contacts.CONTENT_URI,
                null, null, null, null);
    } catch (RemoteException e) {
        // ContentProviderがDeadObject化している。
        // 必要に応じて、ContentProviderClientの再取得とリクエリを実施
        initContentProviderClient();
    } finally {
        if (c != null) {
            try {
                c.close();
            } catch (Exception e) {
                // N.O.P
            }
        }
    }
}

●JellyBeanで拡張されたAPI acquireUnstableContentProviderClient

JellyBeanで下記のAPIが追加されました。

public final ContentProviderClient acquireUnstableContentProviderClient(String)
public final ContentProviderClient acquireUnstableContentProviderClient(Uri)

既存のacquireContentProviderClientメソッドとほぼ同じですが、下記の点で異なります。

ContentProviderClientへの参照を保持している間に、対象のContentProviderが強制終了
してもクライアントプロセスは強制終了されません。
"ContentProviderプロセスの死亡とクライアントプロセスの死亡"
で述べた"道連れ強制終了"が上記のAPIを呼び出して取得したContentProviderClientでは
発生しないということです。

このメソッドは、対象のContentProviderが安全でない、あるいは信頼できない場合に
使用します。

ContentProviderが強制終了された後、このContentProviderにリンクするContentProviderClient
を経由してクエリを発行するとDeadObject例外が投げられます。
その場合、catch節の中で古いContentProviderClientを破棄(release())して、再生成する
必要があります。

サンプルコードは、先ほどのコードにある
acquireContentProviderClient

acquireUnstableContentProviderClient
に置き換えるだけで動作するでしょう。

以上です。