Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2012-03-09

AsyncTaskの使い方考察

| 12:52 | AsyncTaskの使い方考察を含むブックマーク

Androidでの開発ではAndroid OSをある種のフレームワークと捉えてその作法に則ってうまく「使われる」プログラムを書かなくてはならない。なのでそのフレームワークがどういう仕組で、どういう流れで僕らの書いたコードを呼び出すのかということを理解することがよい設計に結びつく。と、大風呂敷を広げているが僕もさほどAndroidに精通していないのでこれは努力目標みたいなもんです。

大雑把に割愛して、今回のテーマに関係の深いところをピックアップする感じでいきますか。

テーマのAsyncTaskだけども、要は非同期処理をしたい時に使う。Javaで非同期処理といえばThreadなんだけどもAndroidでは一般にAsyncTaskを使う。AsyncTaskではUIスレッドを使った非同期処理を簡単に(?)実装できるという触れ込み。

UIスレッドとは?

そこでまずUIスレッドとは何かを理解せねばなるまい。ここでいうUIはユーザインタフェースの略のUIで特に画面を指すGUI(グラフィカル-)とタッチパネル操作などの入力インターフェースを併せてUIという。

で、このUIってのが厄介で、非同期処理したいんだけども状態の不整合がおきて同期が難しくなるってんでよく採用されるのがシングルスレッドモデル。これはUIをいじるスレッドを1つだけに限定して、そのスレッドに細切れにしたUIをいじる処理を渡して順次実行していく、という代物。そのUIをいじる用スレッド(これはフレームワークが作成し管理している)のことをUIスレッドと呼ぶ。

このシングルスレッドモデルはJavaGUISwingなどでも採用されている。GUIフレームワークではメジャーな手法となっている。

Androidの場合はこのUIスレッドに処理をさせるにあたって

  • UIスレッドから呼び出されることになっているメソッド内で処理をする
  • Handler#post()を使う
  • AsyncTaskを使う

あたりがメジャーなところではないかと。

AsyncTaskの概要

AsyncTaskはジェネリックなクラスで

AsyncTask<Params, Progress, Result>

と3つの型変数をもつ。中心となるメソッド

protected abstract Result doInBackground(Params... params)

で、このメソッドの部分が非同期実行される。これはUIスレッドではない。

冒頭の型変数のParamsとResultはこのメソッド引数戻り値で、自分で好きに型を選択できるということだ。paramsはAsyncTaskを実行開始するexecute()メソッド引数に渡されたものがこのdoInBackgroundのparamsとして渡ってくる。

doInBackground()の実行が終わると

protected void onPostExecute(Result result)

が呼び出される。この引数resultは先のdoInBackground()でreturnしたオブジェクトだ。このonPostExecute()はUIスレッドから呼び出される。

つまり、doInBackgroundで非同期処理をして取得した情報をResult型に詰め込んでおき、それをonPostExecute()で受け取ってUIに反映させるという使い方が通常想定される使い方だろう。

特記事項として

void onProgressUpdate(Progress... values)

というメソッドがあって、これはdoInBackground()中でpublishProgress(Progress... values)を呼び出すとUIスレッドがこのonProgressUpdate()を呼び出すというモノ。名前からしてもプログレスバーなどの進捗具合を表示するための機能であることが伺える。

Javaの無名クラス

このAsyncTaskだけども、extends AsyncTaskとしたトップレベルクラスを作ることはあまりない。大抵は無名クラスなどで済ませることが多い。

無名クラスには便利な(そしてややこしい)機能があって、メソッド中で無名クラスを作った場合、finalなローカル変数であれば無名クラスの中で参照することができる。

インスタンスメソッドの中で宣言した無名クラスの中からは、外部クラスのインスタンスフィールドを参照することができる。この際、無名クラスに同名のフィールドがあったりした場合は外部クラス名.this.フィールド名という記法で参照することができる。this.だと無名クラス自身のフィールドを参照することになる。

このあたりは別稿にまとめておいたのでそちらを参照してほしい。

こんな無名クラスを用いてAsyncTaskを使うとどのようになるのか。

AsyncTask使用例

まずはdoInBackground()でint値とString値を渡し、加工して、onPostExecute()にint値とString値を渡し、UIに反映させるような例を考えよう。

class Param {
	int param1;
	String param2;
}
Param param = new Param();

class Result {
	int result1;
	String result2;
}

AsyncTask<Param, Void, Result> task = new AsyncTask<Param, Void, Result>() {
	@Override
	protected Result doInBackground(Param... params) {
		Param param = params[0];

		// 処理をしてonPostExecute()に渡すResult型オブジェクトに格納
		Result result = new Result();
		result.result1 = param.param1;
		result.result2 = param.param2;
		return result; // ここでreturnしたオブジェクトがonPostExecute()に渡される
	}

	@Override
	protected void onPostExecute(Result result) {
		System.out.println(result.result1);
		System.out.println(result.result2);
	}
};
task.execute(param); // パラメータを渡す

ParamとResultはここではローカル内部クラス*1で宣言した。

onProgressUpdate()は今回使わないので型変数Progressは不要だ。こういう場合、java.lang.Voidという型があるのでこれでProgressを潰しておく。

無名クラスを使った場合、外部のメソッドでfinal変数を宣言すれば無名クラス内で参照できるのであった。とするとdoInBackground()の引数pramsを使う必要が薄れる。単一のパラメータを渡すだけならまだアリだが、pramsは型変数Paramsの可変長引数であるからあくまでParams型が複数並んでいるケースでもなければデータを渡すのに使いにくい。

となると可変長引数だからといって配列として渡すなんてのは滅多にやらず、もっぱらParams型オブジェクトのフィールドに渡すべきものを全部並べるという使い方になる。ところが、その場合、Params型を定義しなくてはならず、そんなことならfinal変数作って直接内部で参照してしまえばいいじゃないか、となってしまうわけである。

かくして、無名クラスでのAsyncTaskはParams型変数はどうでもよくなる。execute()の引数にはnullを渡してもいいが、可変長引数なのでそもそも何も渡さないのがシンプルだろう。

さて、Result型をどうするかだが、これはonPostExecute()で使いたいデータを格納するクラスを定義してフィールドに並べるのが妥当だろう。ところが、これまた渡したいデータが複数ある場合にいちいちクラスを宣言するのかよという話になって、無名クラスにフィールド宣言してそこにdoInBackground()でデータを格納してonPostExecute()で参照すればいいんじゃないの、となる。かくしてResult型もどうでもよくなる。

// doInBackgroundに渡したいデータ。final宣言した変数を用いる
final int param1 = 0;
final String param2 = "hoge";

AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
	@Override
	protected Void doInBackground(Void... params) {
		// 処理をしてonPostExecute()に渡すデータをインスタンスフィールドに格納
		result1 = param1;
		result2 = param2;

		return null; // Void型なのでnull値を返しておく
	}

	int result1;
	String result2;

	@Override
	protected void onPostExecute(Void result) {
		System.out.println(result1);
		System.out.println(result2);
	}
};
task.execute(); // ここでは何も渡さない

だいたいこんな書き方をしている人が多いんじゃないだろうか。

連続した処理

非同期処理でやることといえば時間のかかることが中心だ。例えばDBからのデータ読込みとかネットワークからのデータ取得とか。こうした時間のかかる処理をいくつか行わなければならいというケースがままある。そのとき、結果を順次GUIに反映したいとする。最初の時間のかかる処理をAsyncTaskのdoInBackground()→onPostExecute()としてUIに反映させた後にonPostExecute()から次の処理を実行するAsyncTaskを呼び出すという作りになることだろう。

通信してAを読み込んで結果大丈夫であればBを読んで、さらに大丈夫ならCを呼ぶ…といった要求に対してかつて僕は次のようなコードを書いた。

AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
	@Override
	protected Void doInBackground(Void... params) {
		// 処理その1
		// ...
		result1 = 0;
		return null;
	}
	int result1;  // 処理その1の結果

	@Override
	protected void onPostExecute(Void result) {
		// UIスレッドでの処理その1
		// ...
		System.out.println(result1);

		// 処理を終えたら次のタスクを起動する
		AsyncTask<Void, Void, Void> task2 = new AsyncTask<Void, Void, Void>() {
			@Override
			protected Void doInBackground(Void... params) {
				// 処理その2
				// ...
				result2 = "hoge"; // 処理その2の結果
				return null;
			}
			String result2;

			@Override
			protected void onPostExecute(Void result) {
				// UIスレッドでの処理その2
				// ...
				System.out.println(result2);

				// さらに処理を終えたら次のタスクを起動する
				AsyncTask<Void, Void, Void> task3; // 省略
			}
		};
		task2.execute();
	}
};
task.execute();

まぁなんと面倒な事か。

解決策

これについてonProgressUpdate()を利用して実装する方法を考えた。onProgressUpdate()のパラメータ関数を渡せるようにしてしまえばいい。Javaでは関数ポインタのようなものを直接的に扱えないのでここではRunnableインターフェースを用いる。

AsyncTask<Void, Runnable, Void> task = new AsyncTask<Void, Runnable, Void>() {
	@Override
	protected Void doInBackground(Void... params) {
		// 処理その1
		// ...
		final int result1 = 0; // 処理その1の結果
	
		publishProgress(new Runnable() {
			@Override
			public void run() {
				// UIスレッドでの処理その1
				// ...
				// ここでは先の処理その1の結果をfinal変数を用いて受けとって利用できる
				System.out.println(result1);
			}
		});

		// 処理その2
		// ...
		final String result2 = "hoge"; // 処理その2の結果
	
		publishProgress(new Runnable() {
			@Override
			public void run() {
				// UIスレッドでの処理その2
				// ...
				System.out.println(result2);
			}
		});
		
		// 処理その3
		// ...
		return null;
	}

	@Override
	protected void onProgressUpdate(Runnable... values) {
		for (Runnable runnable : values) {
			runnable.run();
		}
	}
};
task.execute();

onProgressUpdate()の実装は引数に渡されたRunnableのrun()を呼び出すだけ。一応、可変長引数になっているのでループで回して順次呼び出すようにしている。

これにより、ネットワークDBなどからデータを読み込んで、読めたものから順次UIに反映させるプログラムが比較的スムーズに書けるのではないか。

まとめ

  • AsyncTaskのパラメータ渡すのは面倒くさい
  • 使い回ししないその場限りのAsyncTaskが比較的多い
  • AsyncTaskを無名クラスで実装する場合、final変数を参照できる機能性を利用するとコーディングが楽
  • さらにonProgressUpdate()にRunnableを渡す作りにすると随時UIスレッドを呼べて良いのではないか

このデザインパターンに対して仮にRunnable型変数AsyncTaskとでも名付けておこうと思う。使い勝手や機能性についての指摘など、ご意見、感想お待ちしております。

2015/04/08 追記

書いてから大分経つがこの記事はよく参照されているようなので、コメントに対する見解を述べておこう。

呼び出し側スコープでfinalな変数を定義し、無名AsyncTaskから参照していますが、

普通こういったものはAsyncTaskのフィールドに持たせ、コンストラクタで渡します。

final変数を無名クラス内で参照した場合、バイトコードレベルで実際に行われることは「フィールドに持たせ、コンストラクタで渡します」と同等だ。つまり、上記の対策は意味がない。

しかし、

AsyncTaskのインスタンスは、可能な限り(そのタスクスレッドの)外部の変数

ライフサイクルに影響されてはいけないのです。

という指摘はまったく正しく、無名クラスのインスタンス内から外部の変数を参照する場合一般の注意事項といえるだろう。

onPostExecute時点でActivityに対して何か行いたいというケースは多いだろうが、この場合は常にActivityがすでにライフサイクルを終えている可能性を考慮しなくてはならない。そして、それは無名クラスからfinal変数を用いて行っても、コンストラクタで渡してAsyncTaskのフィールドに保持していても変わることのない注意事項だ。

とおりすがりとおりすがり 2012/11/14 17:25 非常に危険なコードです。
特に、AsyncTaskのインスタンスよりも長命なオブジェクトに対して
非同期処理結果を反映したい場合はよろしくありません。

呼び出し側スコープでfinalな変数を定義し、無名AsyncTaskから参照していますが、
普通こういったものはAsyncTaskのフィールドに持たせ、コンストラクタで渡します。

AsyncTaskのインスタンスは、可能な限り(そのタスクスレッドの)外部の変数の
ライフサイクルに影響されてはいけないのです。

リンク元