Android

Android Loader, AsyncTaskLoader, CursorLoader のソースコードを読んでまとめてみた

はじめに

CursorLoader はデータ読み込みの結果として Cursor を返すため、1つのテーブルにクエリを発行してデータを取得する場合は便利である。しかし複数のテーブルにアクセスする場合は使い勝手が悪くなる。クエリ時に JOIN 等を使用すればある程度のことはできるが、色々と複雑な処理をしようとすると各テーブルに別々にクエリを発行してプログラムでデータをまとめた方が都合が良い。ただその場合は Cursor を返すのではなく List<ProcessedData> のような感じで結果を返すことになると思うので CursorLoader が使えなくなる。またデータ源がデータベースでない場合などは CursorLoader は使えない。

CursorLoader は AsyncTaskLoader を継承している。さらに AsyncTaskLoader は Loader を継承している。上記のように CursorLoader が使えない場合は AsyncTaskLoader を継承した独自のクラスを実装する必要があるので、これらのクラスについて大まかに理解しておく必要がある。

ソースコードをざっと見たところそれ程複雑ではなさそうなので、各ソースコードを読んで纏めてみることにした。
なお読んだソースコードは android-26/android/support/v4/content/ のディレクトリのソースコードである。

そして最後には AsyncTaskLoader のサブクラスで Cursor ではなく List を返すクラスを実装してみた。

Loader

Loader は Activity/Fragment に表示するデータを読み込むためのものである。Activity/Fragment に関連付けられている LoaderManager と共に動作する。

この Loader - LoaderManager 機構の目的は Activity/Fragment のライフサイクルと Loader オブジェクトのライフサイクルを分離することにある。

このようなことをするのはデータを読み込むには時間がかかる場合があるからである。
画面の回転等で Activity/Fragment が再生成された場合、その度に Loader が再生成されると再度データを読みなおさなくてはならない。特にデータを読み込み中に画面の回転等で Activity/Fragment が再生成された場合は、現在の読み込みを一度中止し Activity/Fragment が再生成されてから改めてデータを読み込むことになり非効率である。

この問題を解決するために Activity/Fragment のライフサイクルと Loader オブジェクトのライフサイクルを分離する。そして Loader 及び Loader によって読み込まれたデータは Activity/Fragment のライフサイクルを超えて保持される。このため Activity/Fragment ではなく LoaderManager が Loader のライフサイクルを管理する。

またロードしたデータを渡す Activity/Fragment は画面回転等によって再生成され異なるオブジェクトとなるが、それも LoaderManager が管理し Loader が読み込んだデータは LoaderManager 経由で適切な Activity/Fragment のオブジェクト(正確には LoaderManager.LoaderCallbacks を実装したオブジェクト)に渡される。

Loaderのライフサイクル

LoaderManager から制御するために Loader は以下のメソッドを持つ。これらのメソッドをユーザーが呼ぶことは LoaderManager の制御とぶつかるため禁じられている。

メソッド 役割
startLoading() データのロードを開始する。通常は Activity/Fragment の onStart のタイミングで呼ばれるが、必ずしも呼ばれるとは限らない。例えば画面回転時に Activity/Fragment が再生成された場合はこのメソッドは呼ばれない。
stopLoading() データのロードを停止する。またこのメソッドが呼ばれたらデータを LoaderManager に渡すことは禁止される。通常は Activity/Fragment の onStop のタイミングで呼ばれるが、必ずしも呼ばれるとは限らない。例えば画面回転時に Activity/Fragment が再生成された場合はこのメソッドは呼ばれない。
abandon() Loader に処理を中止させる。このメソッドが呼ばれた後は Loader は新しいデータを渡してはいけない。そして reset() が呼ばれるまで前回渡したデータを保持する必要がある。
reset() Loader を破棄する。このメソッドが呼ばれたら Loader は状態を初期化し保持していたリソースは全て開放する。但し再度 startLoading が呼ばれたら再び動作しなくてはならない。

Loader のライフサイクルは、動作を観察した限りでは以下のようである。

Lifecycle.png

abandon に関してはドキュメントを読む限りは図の位置で呼ばれるはずであるが、観察した範囲では一度も呼ばれなかった。

stopLoading から startLoading への遷移はHome画面に戻ったり他のアプリケーションに切り替えた後、再度アプリケーションを表示した時に生じた。

画面回転等で Activity/Fragment が再生成された場合はどのメソッドも呼ばれなかった(startLoading が呼ばれた状態のままだった)。

バックボタンでアプリケーションを終了した場合に reset が呼ばれた。
reset は観察した限りでは stopLoading の後に呼ばれていた。

Loader は上の図で示したライフサイクルの状態を表すために以下のフラグを持つ。

名前 意味
boolean mStarted startLoading メソッドが呼ばれ、まだ stopLoading メソッドが呼ばれてない事を示す。
boolean mAbandoned abandon メソッドが呼ばれたことを示す。
boolean mReset reset メソッドが呼ばれたことを示す。

各メソッドを呼び出すことでこれらのフラグは以下のように変化する。

メソッド 状態
初期状態 mReset = true, mAbandoned = false, mStarted = false
startLoading() mReset = false, mAbandoned = false, mStarted = true
stopLoading() mStarted = false
abandon() mAbandoned = true
reset() 初期状態に戻る。

またこれらの状態を取得するためのメソッドがある。

メソッド 戻り値
isStarted() mStarted
isAbandoned() mAbandoned
isReset() mReset

startLoading, stopLoading, abandon, reset メソッドが LoaderManager から呼ばれた時に Loader のサブクラスが何か処理を行うために以下のメソッドが定義されている。サブクラスはこれらをオーバーライドして必要な処理を行う。

メソッド 呼ばれるタイミング
onStartLoading() startLoading() が呼ばれた時。状態を示すフラグが変更された後に呼び出される。
onStopLoading() stopLoading() が呼ばれた時。状態を示すフラグが変更された後に呼び出される。
onAbandon() abandon() が呼ばれた時。状態を示すフラグが変更された後に呼び出される。
onReset() reset() が呼ばれた時。状態を示すフラグが変更される前に呼び出される。

データの読み込み開始と中止

Loader はデータを読み込んだり中止したりするためのメソッドを定義している。

メソッド 役割
forceLoad() データのロードを開始する時に呼ぶ。Loader での実装は単に onForceLoad を呼ぶだけ。
onForceLoad() forceLoad から呼ばれる。サブクラスはこれをオーバーライドしロード開始に必要な処理を行う。
boolean cancelLoad() データのロードを中止するときに呼ぶ。Loader での実装は単に onCancelLoad を呼ぶだけ。ロードの中止に成功したら true を返す。
boolean onCancelLoad() cancelLoad() から呼ばれる。サブクラスはこれをオーバーライドしロードを中止するときに必要な処理を行う。ロードの中止に成功したら true を返す。

読み込み結果の通知

LoaderManager にデータの読み込みが終了したことや読み込みが中止されたことを知らせるために以下のメソッドが用意されている。

メソッド 役割
deliverResult(D data) LoaderManager にロードが終了したことを知らせデータを渡す。正確には registerListener メソッドで登録したリスナーに渡すのだが、通常は LoaderManager がリスナーの登録を行う。
deliverCancellation() LoaderManager にロードが中止されたことを知らせる。正確には registerOnLoadCanceledListener メソッドで登録したリスナーに渡すのだが、通常は LoaderManager がリスナーの登録を行う。
    public void deliverResult(D data) {
        if (mListener != null) {
            mListener.onLoadComplete(this, data);
        }
    }
    public void deliverCancellation() {
        if (mOnLoadCanceledListener != null) {
            mOnLoadCanceledListener.onLoadCanceled(this);
        }
    }

データソースが変更された時の対応

Loader には読み込み先のデータベース等が変更されデータが古くなった場合に再度読み込みを行うための機構が備わっている。
それは内部クラスの ForceLoadContentObserver で ContentObserver を継承している。また static なクラスでないので Loader のプロパティやメソッドにアクセスできる。
ContentObserver はコンストラクタに Handler を渡すと、dispatchChange メソッドを呼び出した時にその Handler に関連付けられたスレッド上で onChange メソッドを呼び出す。よってメインスレッドの Handler を渡しておけば onChange メソッドがメインスレッドで呼ばれる必要がある場合でも dispatchChange メソッドを任意のスレッドから安全に呼び出すことができる。
ForceLoadContentObserver では onChange メソッドをオーバーライドし Loader#onContentChanged() メソッドを呼び出している。

通常はサブクラスのコンストラクタで

    ForceLoadContentObserver observer = new ForceLoadContentObserver();

のようにインスタンス化し、監視対象のデータベース等に渡しておく。データが変更された時に

    observer.dispatchChange(true);

のように呼び出すと、Loaderオブジェクトの onContentChanged() メソッドが呼び出されるしくみになっている。

メソッド 役割
onContentChanged() ForceLoadContentObserver オブジェクトの dispatchChange メソッドを呼び出すと指定のスレッド上で呼び出される。このスレッドは ForceLoadContentObserver オブジェクトを作成したスレッドである。

Loader の onContentChanged メソッドでは、mStarted フラグをチェックし true の場合は forceLoad メソッドを呼び出しデータの読み込みを開始している。
false の場合は LoaderManager からデータのロードを停止されているので mContentChanged フラグに true を設定するのみで終了する。

    public void onContentChanged() {
        if (mStarted) {
            forceLoad();
        } else {
            // This loader has been stopped, so we don't want to load
            // new data right now...  but keep track of it changing to
            // refresh later if we start again.
            mContentChanged = true;
        }
    }

mContentChanged が設定されているかどうかは値を直接参照するのではなく、以下のメソッドを使用する。

    /**
     * Take the current flag indicating whether the loader's content had
     * changed while it was stopped.  If it had, true is returned and the
     * flag is cleared.
     */
    public boolean takeContentChanged() {
        boolean res = mContentChanged;
        mContentChanged = false;
        mProcessingChange |= res;
        return res;
    }

これを見ると mContentChanged フラグが セットされている場合はクリアして mProcessingChanged フラグをセットしている。

そしてデータを読み込んだら以下のメソッドを呼び出し mProcessingChange フラグをクリアする。

    /**
     * Commit that you have actually fully processed a content change that
     * was returned by {@link #takeContentChanged}.  This is for use with
     * {@link #rollbackContentChanged()} to handle situations where a load
     * is cancelled.  Call this when you have completely processed a load
     * without it being cancelled.
     */
    public void commitContentChanged() {
        mProcessingChange = false;
    }

またデータの読み込みを中止した場合は以下のメソッドを呼び出し mContentChanged フラグを再度設定する。

    /**
     * Report that you have abandoned the processing of a content change that
     * was returned by {@link #takeContentChanged()} and would like to rollback
     * to the state where there is again a pending content change.  This is
     * to handle the case where a data load due to a content change has been
     * canceled before its data was delivered back to the loader.
     */
    public void rollbackContentChanged() {
        if (mProcessingChange) {
            mContentChanged = true;
        }
    }

このような複雑な仕組みになっているのはデータの読み込みには時間がかかる場合があり、データの読み込みを非同期で行うことを前提としているからである。takeContentChanged メソッドで元のデータが変更されていることを確認しデータの読み込みを開始しても、終了する前に再度元のデータが変更される可能性がある。その変更を記録するために mContentChanged フラグはデータ読み込み開始時にクリアしておく必要がある。
しかしデータの読み込みが中止された時には再度 mContentChanged フラグをセットしなおす必要があるので、「データソースが変更され再度データを読み込んでいる」という状態を mProcessingChange フラグで記録しておく必要がある。

メソッド 役割
boolean takeContentChanged() データソースが変更されたかどうかを調べる。変更されていれば true を返す。
commitContentChanged() takeContentChanged メソッドでデータソースが変更されたかを確認した後データの読み込みが終了したら呼び出す。
rollbackContentChanged() takeContentChanged メソッドでデータソースが変更されたかを確認したがデータ読み込みを中止した場合に呼び出す。
名前 意味
boolean mContentChanged onContentChanged メソッドが呼ばれたがデータのロードは停止されている時に true に設定し、データが変更されたことを記録する。つまり元となったデータベース等のデータが変化したが、まだそれを読み込まない時にこのフラグが設定される。
boolean mProcessingChange takeContentChanged メソッドが true を返し、その後 commitContentChanged メソッドを呼び出すまでの間 true に設定される。

Loader まとめ

Loader の仕事をまとめると以下のようになる。

  • LoaderManager が Loader のライフサイクルを管理するための startLoading, stopLoading, abandon, reset メソッドを提供する。またこれらが呼ばれたらその状態を記録しておく。
  • さらにサブクラスでこれらのメソッドが呼ばれた時の処理を記述するために onStartLoading, onStopLoading, onAbandon, onReset メソッドを用意し、サブクラスでこれらをオーバライドできるようにする。
  • データの読み込みを開始する forceLoad メソッドを提供する。さらに forceLoad メソッドが呼ばれた時にサブクラスで行う処理を記述するために onForceLoad メソッドを用意する。サブクラスはこれをオーバーライドして処理を記述する。
  • データの読み込みを中止する cancelLoad メソッドを提供する。さらに cancelLoad メソッドが呼ばれた時にサブクラスで行う処理を記述するために onCancelLoad メソッドを用意する。サブクラスはこれをオーバーライドして処理を記述する。
  • LoaderManager に読み込んだデータを渡す deliverResult メソッドを提供する。また読み込みが中止されたことを LoaderManager に通知する deliverCancellation メソッドを提供する。
  • データ提供元が変更された時にデータを読みなおす仕組みとして ForceLoadContentObserver 内部クラスを提供する。
  • データ提供元が変更されているかどうかを調べる takeContentChanged メソッドと、変更されていた場合にデータを読み込み、それが終了したことをコミットするための commitContentChanged メソッド、データの読み込みを中止した際に元の状態に戻す rollbackContentChanged メソッドを提供する。

このように Loader は LoaderManager とのやり取りの仕組みとデータソースが変更された場合の再読み込みの仕組みを提供する。Loader のもう一つの役割である非同期でのデータ読み込みの機能は Loader には存在しない。これを提供するのは以下に説明する AsyncTaskLoader である。

AsyncTaskLoader

データを読み込むには時間がかかる事があるのでメインスレッドではなく別スレッドで行うことが推奨される。特に秒単位以上かかる処理をメインスレッド上で実行すると ANR(Application Not Responding) エラーとされてユーザにアプリを強制終了させるためのダイアログが表示されたりしてしまう。
Loader にはその機構はなく、Loader のサブクラスの AsyncTaskLoader がその仕組みを実装する。

非同期処理

AsyncTaskLoader は AsyncTask を使用して非同期処理を行っている。一方、 v4 サポートライブラリの AsyncTaskLoader はユーザーには非公開のクラス ModernAsyncTask を使用して非同期処理を行っている。両者は実質的に同じと見なして差し支えないので AsyncTask として考える。

ここで AsyncTaskLoader で使っている AsyncTask のメソッドを簡単におさらいする。

AsyncTask#doInBackground は別スレッドで呼び出される。これをオーバーライドして非同期に実行する処理を記述する。

AsyncTask#onPostExecute は非同期の処理が終了するとその結果を引数として受け取る。これをオーバーライドして受け取った結果を処理する。メインスレッド上で呼び出される。

AsyncTask#onCancelled は AsyncTask#cancel メソッドによって非同期の処理が中止され、中止が成功すると呼び出される。これをオーバーライドして受け取った結果を処理する。メインスレッド上で呼び出される。

AsyncTask#executeOnExecutor メソッドでメインスレッド上から非同期処理を開始する。

AsyncTask#cancel メソッドでメインスレッド上から別スレッド上で行われている処理を中止する。なおこのメソッドで自動的に処理が中止されるわけではなく、中止した場合の影響は onPostExecute の代わりに onCancelled が呼び出されることと、isCanceled メソッドが true を返すことのみである。doInBackground 内での処理を中止するには doInBackground 内で isCanceled で確認して自分で処理を中止する必要がある。
cancel メソッドは失敗する場合があるがそれは既に処理が終わっている場合や、既に cancel メソッドが呼ばれている場合などである。

AsyncTask#isCancelled メソッドで処理が中止されたかどうかを確認する。

AsyncTaskLoader は内部クラスとして AsyncTask のサブクラスである LoadTask を定義している。これは static でないので AsyncTaskLoader のプロパティやメソッドにアクセスできる。

データの読み込み開始と中止の実装

データの読み込み開始と中止の具体的な処理を実装するために AsyncTaskLoader では onForceLoad メソッドと onCancelLoad メソッドをオーバーライドしている。

onForceLoad では、まず現在データを読み込み中の場合はそれを中止するよう cancelLoad メソッドを呼び出している。このような場合としてはデータ読み込み中にデータソースが変更され ForceContentObserver オブジェクト経由で forceLoad メソッドが呼ばれた場合等が考えられる。

そして非同期処理を行う TaskLoad オブジェクトを生成し mTask に保存し実行している。なお実行は TaskLoad#executeOnExecutor メソッドを直接呼び出して行うのではなく、executePendingTask メソッドを経由して呼び出している。これは1つには現在中止処理中のタスクがある場合はタスクの実行を延期するためである。それと繰り返しデータを読み込んだ場合その時間間隔を制限するためでもあるが詳細は後述する。

@Override
protected void onForceLoad() {
    super.onForceLoad();
    cancelLoad();
    mTask = new LoadTask();
    executePendingTask();
}

onCancelLoad ではまず現在実行中のタスク mTask に対し mTask.cancel(false) を呼び出す。引数は実行中のスレッドに対し interrupt() メソッドを呼び出すかどうかを指定する。ここでは false を渡しているので実行中のスレッドにはインターラプトはかけられないことになる。

mTask.cancel メソッドによる中止が成功すれば次に mCancellingTask に mTask の値を設定し中止処理をしているタスクを保存する。

そして cancelLoadInBackground メソッドを呼び出している。ここには必要なら別スレッドで実行中の処理をアボートするための処理を記述する。AsyncTaskLoader での実装は空で必要ならサブクラスがオーバーライドして記述するようになっている。

最後に現在実行中のタスク mTask を null でクリアし中止が成功したかどうかを戻り値として返す。

@Override
protected boolean onCancelLoad() {
    if (mTask != null) {
        // 〜 省略 〜

            boolean cancelled = mTask.cancel(false);
            if (cancelled) {
                mCancellingTask = mTask;
                cancelLoadInBackground();
            }
            mTask = null;
            return cancelled;
        }
    }
    return false;
}

現在中止処理を行っている LoadTask オブジェクトがあるかどうかは isLoadInBackgroundCanceled メソッドで確認できる。

public boolean isLoadInBackgroundCanceled() {
    return mCancellingTask != null;
}
プロパティ 役割
mTask 現在非同期にデータを読み込む処理を行っている LoadTask オブジェクトを保持する。
mCancellingTask 現在中止処理を行っている LoadTask オブジェクトを保持する。
メソッド 役割
cancelLoadInBackground サブクラスでオーバーライドし別スレッドで実行中の処理をアボートするための処理を記述する。
isLoadInBackgroundCanceled 現在中止処理を行っている LoadTask オブジェクトがあるか確認する。

読み込み処理の実行

非同期で実行される LoadTask#doInBackground では AsyncTaskLoader#onLoadInBackground() メソッドを呼び出してる。ここでデータの読み込みを実行する。そして読み込んだデータを戻り値として返す。

また cancelLoadInBackground メソッドで処理がアボートされた場合に OperationCanceledException 例外をキャッチするようになっている。

@Override
protected D doInBackground(Void... params) {
    try {
        D data = AsyncTaskLoader.this.onLoadInBackground();
        return data;
    } catch (OperationCanceledException ex) {
        if (!isCancelled()) {
            // onLoadInBackground threw a canceled exception spuriously.
            // This is problematic because it means that the LoaderManager did not
            // cancel the Loader itself and still expects to receive a result.
            // Additionally, the Loader's own state will not have been updated to
            // reflect the fact that the task was being canceled.
            // So we treat this case as an unhandled exception.
            throw ex;
        }
        return null;
    }
}

これを見ると cancelLoadInBackground で処理をアボートした場合は OperationCanceledException 例外を投げることを期待しているように見える。実際 AsyncTaskLoader のサブクラスの CursorLoader ではそのような実装になっている。

また cancelLoadInBackground を呼び出すときは TaskLoader#cancel メソッドを呼び出して成功してからであることが分かる。そうでないと再度例外が投げられてアプリは異常終了する。

onLoadInBackground() では単に loadInBackground() メソッドを呼び出している。これは abstract メソッドでサブクラスはこれをオーバーライドし実際のデータ読み込み処理を記述する。

メソッド 役割
D onLoadInBackground() LoadTask から非同期処理として呼び出される。単に loadInBackground メソッドを呼び出しその戻り値を返してる。
abstract D loadInBackground() onLoadInBackground メソッドから呼ばれる。サブクラスはこれをオーバーライドし読み込み処理を記述する。戻り値は読み込んだデータ。

読み込み結果の通知

非同期のデータ読み込みが終わりデータが返されると LoadTask#onPostExecute メソッドがメインスレッド上で呼ばれ読み込んだデータを受け取る。それはそのまま AsyncTaskLoader#dispatchOnLoadComplete メソッドに渡される。

dispatchOnLoadComplate では以下の処理を行う。

まず commitContentChanged メソッドを呼び出し takeContentChanged メソッドが呼び出されていた場合のコミット処理をしている。

そして現在処理中のタスク mTask を null でクリアし deliverResult メソッドで LoaderManager に結果を通知している。

一方、今回データを読み込んだ LoadTask (引数で渡された task) が既に古い場合が考えられる。これは引数で渡された task と現在処理中の mTask を比較することで判断できる。その場合には後述する dispatchOnCancelled メソッドに渡して task を中止されたものとして扱う。
このような場合としてはデータ読み込み中にデータソースが変更され ForceContentObserver オブジェクト経由で forceLoad メソッドが呼ばれた場合等が考えられる。forceLoad では cancelLoad を呼び出し以前の読み込み処理を中止しているがタイミングが悪いと(処理が既に完了していた場合など)中止に失敗することがある。そのような場合を想定しているものと思われる。

また LoaderManager から abandon メソッドが呼ばれた場合にはデータを渡したり読み込んだデータを保持することはできないので isAbandoned メソッドでチェックし true が返ってきたら onCanceled(data) メソッドを呼び出している。

AsyncTaskLoader での onCanceled の実装は空でサブクラスでオーバーライドしデータ読み込みが中止した場合に必要な処理を記述するようになっている。

void dispatchOnLoadComplete(LoadTask task, D data) {
    if (mTask != task) {
        if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel");
        dispatchOnCancelled(task, data);
    } else {
        if (isAbandoned()) {
            // This cursor has been abandoned; just cancel the new data.
            onCanceled(data);
        } else {
            commitContentChanged();
            mLastLoadCompleteTime = SystemClock.uptimeMillis();
            mTask = null;
            if (DEBUG) Log.v(TAG, "Delivering result");
            deliverResult(data);
        }
    }
}

読み込み中止の通知

一方、LoadTask#cancel メソッドで非同期処理の中止に成功した場合は LoadTask#onPostExecute は呼ばれず、代わりに LoadTask#onCancelled メソッドがメインスレッド上で呼び出される。この場合もデータを受け取るがそれがどのような意味かは実装に依存する。受け取ったデータはそのまま AsyncTaskLoader#dispatchOnCancelled メソッドに渡される。

dispatchOnCancelled メソッドではまず onCanceled メソッドを呼び出している。AsyncTaskLoader での onCanceled の実装は空でサブクラスでオーバーライドしデータ読み込みが中止した場合に必要な処理を記述するようになっている。

引数で渡された task と mCancellingTask と比較し現在中止処理中のタスクであることを確認し rollbackContentChanged メソッドを呼び出して takeContentChanged メソッドが呼び出されていた場合のロールバック処理をしている。

そして現在中止処理中のタスク mCancellingTask を null でクリアし deliverCancellation メソッドで LoaderManager に結果を通知している。

また現在中止処理中のため実行を延期させられていたタスクがある可能性があるので executePendingTask メソッドを呼び出し、実行を再開している。

void dispatchOnCancelled(LoadTask task, D data) {
    onCanceled(data);
    if (mCancellingTask == task) {
        if (DEBUG) Log.v(TAG, "Cancelled task is now canceled!");
        rollbackContentChanged();
        mLastLoadCompleteTime = SystemClock.uptimeMillis();
        mCancellingTask = null;
        if (DEBUG) Log.v(TAG, "Delivering cancellation");
        deliverCancellation();
        executePendingTask();
    }
}
メソッド 役割
dispatchOnLoadComplete ( LoadTask task, D data) 受け取ったデータを LoaderManager に渡すための処理を行う。
dispatchOnCancelled ( LoadTask task, D data) onCanceled メソッドを呼び出しサブクラスで記述された中止処理を行う。そして LoaderManager に読み込みを中止したことを知らせる。
onCanceled ( D data) サブクラスでオーバライドしデータ読み込みの中止に必要な処理を記述する。

実行開始の遅延

AsyncTaskLoader にはデータの読み込みと次の読み込みの時間間隔を制限する機能がある。

setUpdateThrottle メソッドは前回のデータ読み込みが終了または中止されてから次のデータ読み込み開始までの最短の時間間隔を指定する。単位はミリ秒である。
ここで指定した時間間隔未満でデータ読み込みを続けて行うと読み込みはすぐには行われず延期される。

またデータ読み込みを延期するために Handler オブジェクトを生成している。

/**
 * Set amount to throttle updates by.  This is the minimum time from
 * when the last {@link #loadInBackground()} call has completed until
 * a new load is scheduled.
 *
 * @param delayMS Amount of delay, in milliseconds.
 */
public void setUpdateThrottle(long delayMS) {
    mUpdateThrottle = delayMS;
    if (delayMS != 0) {
        mHandler = new Handler();
    }
}

dispatchOnLoadComplete メソッドや dispatchOnCancelled メソッドを見ると mLastLoadCompleteTime に現在の時刻を保存している。
これらの値を使用して指定の時間間隔未満で繰り返しデータ読み込みが行われた時は処理を延期している。
それを行っているのは executePendingTask メソッドである。

executePendingTask ではまず現在中止処理中のタスクがあるかどうか確認している。もしあった場合はタスクの開始処理は行わない。中止処理が終わったら dispatchOnCancelled メソッドから再度呼びだされている。

次に mUpdateThrottle が設定されていたら現在の時刻と mLastLoadCompleteTime + mUpdateThrottle を比較してまだ指定の時間が経過していない場合は処理を延期している。処理の延期は Handler#postAtTime メソッドを用いて行っている。

LoadTask は Runnable インターフェースを実装している。LoadTask#run で再度 executePendingTask を呼び出している。

void executePendingTask() {
    if (mCancellingTask == null && mTask != null) {
        if (mTask.waiting) {
            mTask.waiting = false;
            mHandler.removeCallbacks(mTask);
        }
        if (mUpdateThrottle > 0) {
            long now = SystemClock.uptimeMillis();
            if (now < (mLastLoadCompleteTime+mUpdateThrottle)) {
                // Not yet time to do another load.
                mTask.waiting = true;
                mHandler.postAtTime(mTask, mLastLoadCompleteTime+mUpdateThrottle);
                return;
            }
        }
        if (DEBUG) Log.v(TAG, "Executing: " + mTask);
        mTask.executeOnExecutor(mExecutor, (Void[]) null);
    }
}
final class LoadTask extends ModernAsyncTask<Void, Void, D> implements Runnable {
    // 〜 省略 〜
    boolean waiting;

    @Override
    public void run() {
        waiting = false;
        AsyncTaskLoader.this.executePendingTask();
    }
メソッド名 役割
setUpdateThrottle (long delayMS) 連続したデータ読み込みの最短の時間間隔を設定する。

AsyncTaskLoader まとめ

AsyncTaskLoader の仕事をまとめると以下のようになる。

  • AsyncTask を用いてデータの読み込み処理を別スレッドで実行する。
  • loadInBackground 仮想メソッドを提供する。サブクラスはこれをオーバーライドしデータの読み込み処理を記述する。
  • サブクラスに cancelLoadInBackground メソッドを提供する。サブクラスはこれをオーバーライドし非同期でのデータ読み込み処理をアボートする。
  • サブクラスに onCanceled メソッドを提供する。サブクラスはこれをオーバーライドし読み込みが中止された場合の後処理を行う。
  • データ読み込みの時間間隔を制限するしくみを提供する。

CursorLoader

CursorLoader は AsyncTaskLoader を継承し ContentProvider 経由で非同期にデータを読み込み Cursor を結果として返す。

また Cursor は必要なくなったら close メソッドでリソースを開放する必要があるがその管理も行う。よって CursorLoader を使用する場合はユーザーは Cursor#close 処理は気にしなくて良い。さらに言えばユーザーは CursorLoader から得た Cursor オブジェクトの close メソッドを読んではいけない。

クエリパラメータの設定

CursorLoader では ContentProvider にクエリを投げる時のパラメータをコンストラクタで指定する。

public CursorLoader(Context context, Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder)

クエリパラメータの無いコンストラクタも存在するがその場合はセッターで必要なパラメータを設定する必要がある。

ForceLoadContentObserver オブジェクトの作成

コンストラクタでは Loader#ForceLoadContentObserver オブジェクトが生成され mObserver に保存される。これを用いてデータソースが変更された時には CursorLoader に再度データを読みこませる。

データ読み込みの開始

CursorLoader は onStartLoading メソッドをオーバライドし LoaderManager から startLoading を呼ばれた時の処理を記述している。

まず前回読み込んだ結果が mCursor に保存している場合はそれを deliverResult メソッドで LoaderManager に渡す。

そして takeContentChanged メソッドでデータソースが変更されていないかをチェックして、そうであれば forceLoad メソッドでデータを読み込んでいる。
まだ一度もデータを読み込んでいない場合も forceLoad でデータを読み込んでいる。

ここで takeContentChanged が true を返す場合でも前回の結果を LoaderManager に渡している点に注意。こうすることでユーザーは待たされることなく直ぐにデータを見ることができる。データの読み込みが終わったら新しいデータが表示される。

/**
 * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks
 * will be called on the UI thread. If a previous load has been completed and is still valid
 * the result may be passed to the callbacks immediately.
 *
 * Must be called from the UI thread
 */
@Override
protected void onStartLoading() {
    if (mCursor != null) {
        deliverResult(mCursor);
    }
    if (takeContentChanged() || mCursor == null) {
        forceLoad();
    }
}

データ読み込みの中止

CursorLoader は onStopLoading メソッドをオーバライドし LoaderManager から stopLoading を呼ばれた時の処理を記述している。

ここでは単に cancelLoad メソッドを呼び出して読み込みを行っている場合は処理を中止している。

@Override
protected void onStopLoading() {
    // Attempt to cancel the current load task if possible.
    cancelLoad();
}

また CursorLoader は cancelLoadInBackground メソッドをオーバーライドし非同期の読み込みをアボートする処理を記述している。
ここでは後述する loadInBackground メソッドで生成される CancellationSignal オブジェクト mCancellationSignal の cancel メソッドを呼び出している。

    @Override
    public void cancelLoadInBackground() {
        super.cancelLoadInBackground();

        synchronized (this) {
            if (mCancellationSignal != null) {
                mCancellationSignal.cancel();
            }
        }
    }

非同期でのデータ読み込み

CursorLoader では AsyncTaskLoader#loadInBackground メソッドをオーバーライドしてデータの読み込み処理を記述している。

まず isLoadInBackgroundCanceled メソッドで非同期処理が始まる前に cancelLaod メソッドで処理が中止されていないかを確認する。されていたら OperationCanceledException 例外を投げる。これは AsyncTaskLoader.LoadTask#doInBackground 内でキャッチされる。

次に ContentResolver 経由でクエリパラメータを用いて ContentProvider にクエリを投げる。その際 CancellationSignal オブジェクトもクエリにつける。

CancellationSignal オブジェクトは ContentResolver, ContentProvider 経由で最終的に SQLiteDatabase#query 等に渡される(SQLiteデータベースをデータソースとして使用している場合)。その場合 CancellationSignal#cancel メソッドを呼び出すと query は中断され OperationCanceledException 例外が投げられる。

Cursor が取得できたら Cursor#registerContentObserver メソッドで ForceLoadContentObserver オブジェクトを登録する。Cursor 内でこのオブジェクトがどのように使われているかは追わないが Cursor#setNotificationUri (ContentResolver cr, Uri uri) メソッドで ContentResolver と Uri を登録すると ContentResolver#notifyChange (Uri uri, ContentObserver null) 等でこの ForceLoadContentObserver オブジェクト経由で変更を通知できるようになる。

そして最後に mCancellationSignal オブジェクトを開放している。

@Override
public Cursor loadInBackground() {
    synchronized (this) {
        if (isLoadInBackgroundCanceled()) {
            throw new OperationCanceledException();
        }
        mCancellationSignal = new CancellationSignal();
    }
    try {
        Cursor cursor = ContentResolverCompat.query(getContext().getContentResolver(),
                mUri, mProjection, mSelection, mSelectionArgs, mSortOrder,
                mCancellationSignal);
        if (cursor != null) {
            try {
                // Ensure the cursor window is filled.
                cursor.getCount();
                cursor.registerContentObserver(mObserver);
            } catch (RuntimeException ex) {
                cursor.close();
                throw ex;
            }
        }
        return cursor;
    } finally {
        synchronized (this) {
            mCancellationSignal = null;
        }
    }
}

読み込み結果の通知

CursorLoader では deliverResult メソッドをオーバーライドしている。

データを読み込んだ結果の Cursor が返ってきたらまず isReset メソッドで LoaderManager から reset メソッドが呼ばれていないかを確認する。そうなら Loader は既に破棄されているので返ってきた Cursor の close 処理を行う。
そうでなければ isStarted メソッドで LoaderManager が値を渡すのを許可しているのを確認し super.deliverResult で結果を渡す。
そして古い Cursor があればそれの close 処理を行う。

@Override
public void deliverResult(Cursor cursor) {
    if (isReset()) {
        // An async query came in while the loader is stopped
        if (cursor != null) {
            cursor.close();
        }
        return;
    }
    Cursor oldCursor = mCursor;
    mCursor = cursor;

    if (isStarted()) {
        super.deliverResult(cursor);
    }

    if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
        oldCursor.close();
    }
}

ちょっと気になったのは LoaderManager から abandon が呼ばれている時にこのメソッドが呼ばれても結果の Cursor を保存しているところである。通知も保存もしてはいけない規約であるはず。

読み込みが中止された時の後処理

CursorLoader では AsyncTaskLoader#onCancled メソッドをオーバーライドしてデータの読み込みが中止された場合の処理を記述している。ここでは中止された処理から返ってきた Cursor の close を行っている。

@Override
public void onCanceled(Cursor cursor) {
    if (cursor != null && !cursor.isClosed()) {
        cursor.close();
    }
}

CursorLoader の破棄

LoaderManager から reset が呼ばれた時の処理を onReset メソッドをオーバライドして記述している。
まず onStopLoading メソッドを呼び出しデータ読み込みを停止する。reset メソッドの前に stopLoading メソッドが呼ばれていれば必要ないはずであるが、 startLoading メソッドのすぐ後にも reset メソッドが呼ばれる可能性があるのかもしれない。
そして mCursor の close 処理を行い mCursor を null でクリアして保持していた Cursor オブジェクトを開放している。

@Override
protected void onReset() {
    super.onReset();

    // Ensure the loader is stopped
    onStopLoading();

    if (mCursor != null && !mCursor.isClosed()) {
        mCursor.close();
    }
    mCursor = null;
}

CursorLoader のまとめ

CursorLoader の仕事をまとめると以下のようになる。

  • loadInBackground メソッドをオーバーライドし ContentProvider にクエリを投げる。
  • クエリ結果のカーソルを保持し LoaderManager に提供する。また適切に close 処理を行う。
  • ForceLoadContentObserver オブジェクトを作成しデータソースが変更された時にデータを再読み込みできるようにする。

AsyncTaskLoader を継承して Loader クラスを書いてみる

戻り値に Cursor の様に close 処理などをせず単に捨てれば良い場合の Loader クラスを書いてみる。
それには CursorLoader クラスを参考にして Cursor#close メソッドを呼んでいる場所を削除したようなクラスをかけば良い。
クラス名を AsyncLoaderBase とする。 abstract クラスとしこれを継承して loadInBackground メソッドをオーバーライドして使用する。

package replace.to.your.package.name;

import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;

public abstract class AsyncLoaderBase<D> extends AsyncTaskLoader<D> {
    private D data;

    public AsyncLoaderBase(Context context) {
        super(context);
    }

    @Override
    public void deliverResult(D data) {
        if (isReset()) return;

        // ロード結果を保持する。
        this.data = data;

        // Activity/Fragment が onStart - onStop の間の状態ならロード結果を送信する。
        if (isStarted()) {
            super.deliverResult(data);
        }
    }

    @Override
    protected void onStartLoading() {
        // 前回のロード結果があればそれを送信する。
        if (data != null) {
            deliverResult(data);
        }
        // コンテンツが変更されたかを調べ、そうならロードを行う。
        // またはまだロードが一度も行われていなければロードする。
        // コンテンツが読み込まれるまでは前回の結果が表示される。
        boolean contentChanged = takeContentChanged();
        if (contentChanged || data == null) {
            forceLoad();
        }
    }

    @Override
    protected void onStopLoading() {
        // ここでcancelLoadを呼ぶとまだロードが終了していない場合は中止される。
        // ここで中止するかどうかは意見が別れると思う。
        // ユーザーが他にやりたい事があってアプリを閉じた場合はこのように中止した
        // 方が良いだろう。
        // 一方、ロードが終わるまでとりあえず他のアプリを使いたい場合等は
        // ここで中止するといつまでたってもロードが終わらない事になる。
        // 一般的にはロードのコストがそれ程高くない場合はこのように中止して、
        // ロードにかなり時間がかかる場合などはあえてcancelLoadを呼ばないという
        // 実装もありだと思う。
        cancelLoad();

    }

    @Override
    protected void onReset() {
        super.onReset();
        // 念の為これを呼ぶ。
        onStopLoading();
        // 前回のロード結果を破棄する。
        data = null;
    }
}

例えば DataRepository クラスなるものがあって以下のメソッドが実装してあるとする。簡単のためこれらは static メソッドとする。

  • DataRepository#getData メソッドで List<Data> 型のデータを読み込む。
  • DataRepository#registerContentObserver メソッドで ContentObserver を登録しデータソースの変更が監視できる。
  • DataRepository#unregisterContentObserver メソッドで ContentObserver の登録を解除する。

その場合これにアクセスするクラスは

// package, import 文は省略

public final class MyLoader extends AsyncLoaderBase<List<Data>> {
    private Loader.ForceLoadContentObserver observer;

    public MyLoader(Context context) {
        super(context);
        // ForceLoadContentObserver を DataRepository に登録する。
        observer = new Loader.ForceLoadContentObserver();
        DataRepository.registerContentObserver(observer);
    }

    @Override
    public List<Data> loadInBackground() {
        // CursorLoaderと同じにする。
        synchronized (this) {
            if (isLoadInBackgroundCanceled()) {
                throw new OperationCanceledException();
            }
        }

        // DataRepository からデータを取得する。
        return DataRepository.getData();
    }

    @Override
    protected void onReset() {
        // ForceLoadContentObserver を DataRepository から削除する。
        DataRepository.unregisterCardsObserver(observer);
        super.onReset();
    }
}

のようなものになる。

なお簡単のため CancellationSignal を用いたアボート処理は省いている。必要なら CursorLoader のように実装すれば良い。

DataRepository 側ではデータが変更された場合は渡された ContentObserver の dispatchChange メソッドを呼びだせば MyLoader オブジェクトに変更が通知されデータが再度読み込まれる。