2012年11月5日月曜日

Android - AsyncTaskLoader

Loaderを使用する意図
今まで、HTTP通信などの時間がかかる処理を行う場合はAsyncTaskを用いて非同期処理を行い、SQLiteデータベースとのやり取りを行う場合はCursorを用いてstartManagingCursor()で Activityのライフサイクルに依存させるのが定番の実装であった。 が、AsyncTaskの場合はUIスレッドで行う処理を記述するonPreExecute()やonPostExecute()の実装がどうしてもActivityに依存してしまう為、 各Activityのインナークラスとして定義せざるを得なかった。その為、筆者は「元となる共通化されたAsyncTaskを定義し、それをextendsしたものを各Activity内でインナークラスとして定義する」 といったような実装をしていた。こうすると、各Activity内ではonPreExecute()とonPostExecute()を実装したクラスを定義すれば良くなる。だがこれはあくまでextendsしたものであるので、 親クラスの実装に依存する。

キャッシュを利用したい時はinitLoader()、最新を取得したい時はrestartLoader()を使い分けることによって、半永続的なキャッシュ処理が容易になる
非同期部分(共通化できる部分)はLoaderに定義し、各ActivityやFragmentではそれらに依存する処理をLoaderCallbacks<T>を implementsして定義することにより、MVCモデルにおけるモデルとコントローラの分離ができる
CursorLoaderに関しては、今までUIスレッドで行なっていたSQLiteとの通信処理を非同期で行う事により、 何度もクエリを投げなければならない場合などのまさかのANRの回避及びユーザビリティの向上に繋がる
という事だと思う。CursorLoaderを使用する場合はSQLiteとのやり取りがコンテンツプロバイダでwrapされていることが前提なので、行儀が良くなる事も長所かもしれない。 多分今までコンテンツプロバイダを利用する事はあっても、SQLiteを利用する為に自分で作成したという人はあまりいないだろう。 よくコンテンツプロバイダの説明で「他のアプリとのデータの共有(外部にデータを公開するインターフェースを定義する)」とか書いてあるが、その用途でアプリを作成している事は稀だからだ。 が、常にコンテンツプロバイダを介する事により、「実装の隠蔽(直接クエリを投げない)」という意味では行儀が良いのは間違いない。
ちなみに、AndroidManifest.xmlの定義でコンテンツプロバイダにしたとしてもそれをアプリ内部からしか見えないようにする事は可能なので安心。

まずは公式の翻訳を読もう。 大体の使い方は分かるが、もっと具体的な実装の流れを知りたいと思うだろう。その場合は以下の記事に付き合って頂きたい。

AsyncTaskLoaderの各メソッド
AsyncTaskの代わりになるのがこのAsyncTaskLoaderだ。しかしこのAsyncTaskLoaderのメソッドをどう使うべきなのかよくわからない。あとLoaderCallbacks<T>との兼ね合いもよく分からない。 ので、以下のようにログを埋め込んで試してみた。対象は、ドキュメントに「サブクラスでオーバーライドせよ」と書いてあるonXXX系のメソッドをフレームワークのソースを見ながら「何となく」選び、 あとLoaderCallbacks<T>のメソッドとする。各メソッドがどのタイミングで呼ばれ、どういう動きをするのか追ってみる。
以下、着目すべきはAsyncTaskLoaderの方には共通的な処理を書き、LoaderCallbacks<T>には各ActivityやFragmentに依存した処理を書く前提という事である。 例えば、通信を行うときにProgressDialogを出す→通信→ダイアログ閉じる、のような処理はどう挟み込むべきか、といった感じである。

注意すべきはAsyncTaskLoaderのonStartLoading()にforceLoad()を書いていることだ。これを書かないとロードが始まらない。
ここに書くのとLoaderCallbacksのonCreateLoader()に書く流儀があるようだが、Loaderの方は共通処理、Callbacksの方はビジネスロジック依存、
ということから考えるとLoaderに定義する方が勝ると思う。公式のCursorLoaderのソースもそうなっているし。

public class MainActivity extends FragmentActivity implements LoaderCallbacks<String>{
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
LoaderManager.enableDebugLogging(true);
findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().initLoader(0, null, MainActivity.this);
}
});
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().restartLoader(0, null, MainActivity.this);
}
});
findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().destroyLoader(0);
}
});
findViewById(R.id.button4).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
Loader<Object> loader = getSupportLoaderManager().getLoader(0);
if (loader != null) {
AsyncTaskLoader<?> loader2 = (AsyncTaskLoader<?>)loader;
loader2.cancelLoad();
}
}
});
}

@Override
public Loader<String> onCreateLoader(int id, Bundle args) {
Log.d(getClass().getSimpleName(), "onCreateLoader.");
return new MyLoader(this);
}

@Override
public void onLoadFinished(Loader<String> loader, String result) {
Log.d(getClass().getSimpleName(), "onLoadFinished.");
}

@Override
public void onLoaderReset(Loader<String> loader) {
Log.d(getClass().getSimpleName(), "onLoaderReset.");
}

static class MyLoader extends AsyncTaskLoader<String> {
public MyLoader(Context context) {
super(context);
}

@Override
public String loadInBackground() {
Log.d(getClass().getSimpleName(), "loadInBackground start.");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(getClass().getSimpleName(), "loadInBackground end.");
return "";
}

@Override
protected void onAbandon() {
Log.d(getClass().getSimpleName(), "onAbandon.");
}

@Override
public void onCanceled(String data) {
Log.d(getClass().getSimpleName(), "onCanceled.");
}

@Override
protected void onReset() {
Log.d(getClass().getSimpleName(), "onReset.");
}

@Override
protected void onStartLoading() {
Log.d(getClass().getSimpleName(), "onStartLoading.");

// これを入れないとonStartLoadingで止まってしまう.
forceLoad();
}

@Override
protected void onStopLoading() {
Log.d(getClass().getSimpleName(), "onStopLoading.");
}
}
}
実行していないIDに対しinitLoader()

05-10 12:54:28.915: V/LoaderManager(7685): initLoader in LoaderManager{40554fc8 in MainActivity{4054ed20}}: args=null
05-10 12:54:28.915: D/MainActivity(7685): onCreateLoader.
05-10 12:54:28.915: V/LoaderManager(7685): Starting: LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:54:28.915: D/MyLoader(7685): onStartLoading.
05-10 12:54:28.915: V/LoaderManager(7685): Created new loader LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:54:28.915: D/MyLoader(7685): loadInBackground start.
05-10 12:54:31.918: D/MyLoader(7685): loadInBackground end.
05-10 12:54:31.918: V/LoaderManager(7685): onLoadComplete: LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:54:31.918: V/LoaderManager(7685): onLoadFinished in MyLoader{40555610 id=0}: String{40028418}
05-10 12:54:31.918: D/MainActivity(7685): onLoadFinished.
Callbacks.onCreateLoader() → Loader.onStartLoading() → Loader.loadInBackground → Callbacks.onLoadFinished().

既に実行したIDに対しinitLoader()

05-10 12:55:10.735: V/LoaderManager(7685): initLoader in LoaderManager{40554fc8 in MainActivity{4054ed20}}: args=null
05-10 12:55:10.735: V/LoaderManager(7685): Re-using existing loader LoaderInfo{405555a8 #0 : MyLoader{40555610}}
05-10 12:55:10.735: V/LoaderManager(7685): onLoadFinished in MyLoader{40555610 id=0}: String{40028418}
05-10 12:55:10.735: D/MainActivity(7685): onLoadFinished.
Callbacks.onLoadFinished()が呼ばれて終了する。既にデータは取得済なので、キャッシュが利用されているのがわかる。

実行していないIDに対しrestartLoader()

05-10 12:55:54.038: V/LoaderManager(7685): restartLoader in LoaderManager{40560158 in MainActivity{40559b90}}: args=null
05-10 12:55:54.038: D/MainActivity(7685): onCreateLoader.
05-10 12:55:54.048: V/LoaderManager(7685): Starting: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:55:54.048: D/MyLoader(7685): onStartLoading.
05-10 12:55:54.068: D/MyLoader(7685): loadInBackground start.
05-10 12:55:57.071: D/MyLoader(7685): loadInBackground end.
05-10 12:55:57.071: V/LoaderManager(7685): onLoadComplete: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:55:57.071: V/LoaderManager(7685): onLoadFinished in MyLoader{405607f0 id=0}: String{40028418}
05-10 12:55:57.071: D/MainActivity(7685): onLoadFinished.
Callbacks.onCreateLoader() → Loader.onStartLoading() → Loader.loadInBackground → Callbacks.onLoadFinished(). initLoader()を呼んだ時と同じ動作。

既に実行したIDに対しrestartLoader()

05-10 12:56:13.657: V/LoaderManager(7685): restartLoader in LoaderManager{40560158 in MainActivity{40559b90}}: args=null
05-10 12:56:13.657: V/LoaderManager(7685): Making last loader inactive: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:56:13.657: D/MyLoader(7685): onAbandon.
05-10 12:56:13.657: D/MainActivity(7685): onCreateLoader.
05-10 12:56:13.657: V/LoaderManager(7685): Starting: LoaderInfo{40561f50 #0 : MyLoader{40561fb8}}
05-10 12:56:13.657: D/MyLoader(7685): onStartLoading.
05-10 12:56:13.657: D/MyLoader(7685): loadInBackground start.
05-10 12:56:16.670: D/MyLoader(7685): loadInBackground end.
05-10 12:56:16.670: V/LoaderManager(7685): onLoadComplete: LoaderInfo{40561f50 #0 : MyLoader{40561fb8}}
05-10 12:56:16.670: V/LoaderManager(7685): onLoadFinished in MyLoader{40561fb8 id=0}: String{40028418}
05-10 12:56:16.670: D/MainActivity(7685): onLoadFinished.
05-10 12:56:16.670: V/LoaderManager(7685): Destroying: LoaderInfo{40560788 #0 : MyLoader{405607f0}}
05-10 12:56:16.670: D/MyLoader(7685): onReset.
Loader.onAbandon() → Callbacks.onCreateLoader() → Loader.onStartLoading() → Loader.loadInBackground → Callbacks.onLoadFinished() → Loader.onReset().
古いローダの利用を止める→新しいローダ実行、取得→古いローダを終了、という流れのようだ。ログを見る限りCallbacks.onLoaderReset()は呼ばれない事に注意。

実行していないIDに対しdestroyLoader()

05-10 12:57:04.476: V/LoaderManager(7685): destroyLoader in LoaderManager{4056b978 in MainActivity{405654f0}} of 0
実行していないので、当然何も起こらない。

既に実行したIDに対しdestroyLoader()

05-10 12:57:38.249: V/LoaderManager(7685): destroyLoader in LoaderManager{4056b978 in MainActivity{405654f0}} of 0
05-10 12:57:38.249: V/LoaderManager(7685): Destroying: LoaderInfo{4056c848 #0 : MyLoader{4056c8b0}}
05-10 12:57:38.249: V/LoaderManager(7685): Reseting: LoaderInfo{4056c848 #0 : MyLoader{4056c8b0}}
05-10 12:57:38.249: D/MainActivity(7685): onLoaderReset.
05-10 12:57:38.249: D/MyLoader(7685): onReset.
Callbacks.onLoaderReset() → Loader.onReset(). 今度はCallbacks.onLoaderReset()が呼ばれた。再利用ではなく完全に破棄の場合に呼ばれるようだ。

Loader実行後に画面を閉じる(ActivityのonDestroy()発生)

05-10 13:00:54.601: V/LoaderManager(7685): Stopping in LoaderManager{4056b978 in MainActivity{405654f0}}
05-10 13:00:54.601: V/LoaderManager(7685): Stopping: LoaderInfo{4056ee90 #0 : MyLoader{4056eef8}}
05-10 13:00:54.601: D/MyLoader(7685): onStopLoading.
05-10 13:00:54.601: V/LoaderManager(7685): Destroying Active in LoaderManager{4056b978 in MainActivity{405654f0}}
05-10 13:00:54.601: V/LoaderManager(7685): Destroying: LoaderInfo{4056ee90 #0 : MyLoader{4056eef8}}
05-10 13:00:54.611: V/LoaderManager(7685): Reseting: LoaderInfo{4056ee90 #0 : MyLoader{4056eef8}}
05-10 13:00:54.611: D/MainActivity(7685): onLoaderReset.
05-10 13:00:54.611: D/MyLoader(7685): onReset.
05-10 13:00:54.611: V/LoaderManager(7685): Destroying Inactive in LoaderManager{4056b978 in MainActivity{405654f0}}
Loader.onStopLoading() → Callbacks.onLoaderReset() → Loader.onReset().
Loader.onStopLoading()が初めて出てきた。が、あまり使わないのかも。

loadInBackground実行中にLoader.cancelLoad()

05-10 13:27:26.012: V/LoaderManager(8516): initLoader in LoaderManager{40546be8 in MainActivity{4053f818}}: args=null
05-10 13:27:26.012: D/MainActivity(8516): onCreateLoader.
05-10 13:27:26.012: V/LoaderManager(8516): Starting: LoaderInfo{405471c8 #0 : MyLoader{40547230}}
05-10 13:27:26.012: D/MyLoader(8516): onStartLoading.
05-10 13:27:26.012: V/LoaderManager(8516): Created new loader LoaderInfo{405471c8 #0 : MyLoader{40547230}}
05-10 13:27:26.022: D/MyLoader(8516): loadInBackground start.
05-10 13:27:29.025: D/MyLoader(8516): loadInBackground end.
05-10 13:27:29.025: D/MyLoader(8516): onCanceled.
(一連のロード開始処理)→ Loader.loadInBackground → Loader.onCanceled.
当然ながらloadInBackgroundは最後まで実行されるのだが、最後まで実行されてからonCanceledが呼ばれるようだ。
ちなみにgetLoader()で対象IDをのローダが無い場合はnullが返るので、上記コードのようにnullチェックを行うこと。

ちなみに

ActivityのonPause()やonResume()のタイミングでも自動で何かやってくれるのかと思って試してみたが、何も起きなかった。
更に、initLoader()を連打した場合は、結局ロードは1回しか行われず、連打分だけ再利用されたが、Callbacks.onLoadFinished()は一度だけ呼ばれた。
restartLoader()を連打した場合は、その連打回数だけロードされるが、「直列に」行われた(前のローダが終了してから、次のローダが開始された)
一応書くが、すべて「同一IDのローダの場合」である。IDが違う場合は、それぞれ「並列に」ロードされた。

まとめると
LoaderCallbacks<T>

onCreateLoader() ローダ新規作成時(=新しいデータを取りに行く時)に呼ばれる。次に必ず長時間の非同期処理が走る。 ProgressDialogなどの経過処理を出すならここで出すべき。
onLoadFinished() 「新しい、古い関係なく」データ取得完了時に呼ばれる。ProgressDialogなどの経過処理を非表示にする処理をここに書くが、 「表示されていない場合もエラーにならない」ように書くべき。
onLoaderReset() 「ローダが完全に破棄」される場合に呼ばれる。「ローダ再利用(古いローダを破棄)」の場合は呼ばれない。リソース開放などの処理を書くべきか。
AsyncTaskLoaderの方のメソッドのオーバーライドは細かく動作を制御したい時のみオーバライドすれば良いかも。前述の通り、onStartLoading()にforceLoad()だけ書こう。
ロードのキャンセル処理も、結局UIの方でLoader.cancelLoad();を明示的に呼ぶので、そこで何かアクションをすればいいはず。 Loader.onCanceled()の方では、もし必要であれば、例えば既にキャッシュ用のリソースにデータを書き込んでしまった場合は、それを巻き戻すとか。(イマイチな例かも。そもそもloadInBackground内でキャッシュ処理書かないだろうし)

つまり、実践例としては以下のような雰囲気になるだろう。(cancelとdestroyは端折って最小構成にしてある)

public final class MainActivity extends FragmentActivity {

/** 個人的にはActivity / Fragmentにimplementsするよりこのようにprivateフィールドにした方が見通しが良くなる気がする. */
private final LoaderCallbacks<String> mLoaderCallbacks = new LoaderCallbacks<String>() {
@Override
public Loader<String> onCreateLoader(int id, Bundle bundle) {
// プログレスダイアログ表示.
ProgressDialogFragment dialog = new ProgressDialogFragment();
Bundle args = new Bundle();
args.putString("message", "データを読み込んでいます。");
dialog.setArguments(args);
dialog.show(getSupportFragmentManager(), "progress");
return new MyLoader(MainActivity.this);
}

@Override
public void onLoadFinished(Loader<String> loader, String result) {
// プログレスダイアログを安全に閉じる.
ProgressDialogFragment dialog = (ProgressDialogFragment)getSupportFragmentManager().findFragmentByTag("progress");
if (dialog != null) {
dialog.onDismiss(dialog.getDialog());
}

// TODO ここでUIへのデータ更新処理を行う.
}

@Override
public void onLoaderReset(Loader<String> loader) {
// TODO ここでデータの破棄の処理を行う. 基本書かなくていいかも.
}
};

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// LoaderManagerに関するデバッグログON. 動作を追うのに有用. 本番ビルド時は外そう.
LoaderManager.enableDebugLogging(true);

// リロードボタン. キャッシュを破棄し最新を取得する.
findViewById(R.id.reload).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getSupportLoaderManager().restartLoader(0, null, mLoaderCallbacks);
}
});

// キャッシュがある場合はそれを使用しつつ, データを読み込む.
getSupportLoaderManager().initLoader(0, null, mLoaderCallbacks);
}

/** 今回のメインとなるAsyncTaskLoader. */
public static class MyLoader extends AsyncTaskLoader<String> {

/** コンストラクタ. */
public MyLoader(Context context) {
super(context);
}

/** 非同期で長い処理を行う. */
@Override
public String loadInBackground() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}

/** 決まり文句のように書いて良い. 「こういう場合はinit/restartLoader呼ばれたけど読み込んでほしくない」という時はその条件を入れる. */
@Override
protected void onStartLoading() {
forceLoad();
}
}

/** プログレスダイアログ用のDialogFragment. 本来は別ファイルに書くが. */
public static class ProgressDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
ProgressDialog dialog = new ProgressDialog(getActivity());
dialog.setMessage(getArguments().getString("message"));
return dialog;
}
}
}
ちなみに
よくLoaderのメソッドであるdeliverResult()やその他onXXX系をオーバーライドして使用しているソースを見かけるが、 恐らくCursorLoaderの実装を見て真似ているのだと思うのだが、ContentProviderを絡めずにただ単にAsyncTaskの代替手段として用いるのであれば、 特にその他のメソッドをオーバーライドする必要はない。Loaderのソースを読めばわかるが、takeContentChanged()もContentProvider前提なので気にする必要はない。 また、CursorLoaderがフィールドにCursorの参照を持っているのは自前でCursor.close()の管理をしたいだけなので、AsyncTaskLoaderでこれの真似をする必要はない。 フィールドに持たなくても既に実行されたidに対しinitLoader()すれば前回の結果が返るので心配はない。 故に上記ソースのMyLoaderが最小構成である。

@Override
protected void onStopLoading() {
cancelLoad();
}
しかしこれは入れたほうがいいかもしれないと思った。上記調査から、onStopLoading()はLoader実行後に画面を閉じる(ActivityのonDestroy()発生)の時の呼ばれるので、 cancelLoad()しておけばとりあえずLoaderCallbacks.onLoadFinished()は呼ばれない。

0 件のコメント:

コメントを投稿