Handlerクラスの正しい使い方(Androidでスレッド間通信)

AndroidのHandlerクラスは別スレッドからUI部品操作を用いる際に、よく使われる。Androidの場合はUIスレッドからでないとUI部品を操作できないという制約がある。どのサイトを見てもUIスレッドへイベントを送るための仕組みとして語られている。

いやいや、それは事実だが、それだけでない。

Handlerクラスはスレッド間通信のための仕組みである。もっと正確に言うと、Handlerインスタンスを生成したスレッドへイベントを送るための仕組みなのである。当たり前だと思う人も多いかもしれないが、多くの人はこのことを理解できていない。

ソースレベルで説明してみる。よく書かれるソースは以下のような感じである。

Handler handler = new Handler(); // (1)
handler.post(new Runnable() {
	@Override
	public void run() {
		// UI部品への操作;
		return;
	}
});

このソースのポイントは(1)である。(1)を実行したスレッドへHandler.post()で指定したRunnableが送られる。つまり、UIスレッドで(1)を実行すれば、UIスレッドへ送られ、別スレッドで(1)を実行すれば、別スレッドへ送られる。

では、その実体をAndroidのソースHandlerのコンストラクタのソースを見て、確認してみる。

    public Handler() {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper(); // ポイント(1)
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue; // ポイント(2)
        mCallback = null;
    }

ポイントは2点。ポイント(1)で呼び出しスレッドの宛先(Looper)を取り出し、ポイント(2)で宛先のポスト(mQueue)へRunnableを送りつけている。このソースを見るとよくわかるのだが、Handlerクラスは決してUIスレッドへRunnableを送りつけるためのものではない。Handlerインスタンスを生成したスレッドへイベントを送りつけるための仕組みである。

Runnableを送りつけたい宛先を明示したい場合は、もう一つのコンストラクタであるHandler(Looper looper)を使う。これを使えば、任意のThreadへRunnableを送りつけることができる。

この仕組みさえ知っていれば、任意のスレッドとの通信が可能となる。例えば、WebViewを使っている場合は、WebViewの本体であるWebViewCoreThreadと通信することが可能となる。

例えば、こんな感じ。

public class HogeTask extends Thread {
	public HogeTask() {
	}
	
	public void execute(Looper toLooper, String data) {

		// ここでスレッドでしたい処理を記述

		new Handler(toLooper).post(new Runnable() {
			public void run() {

				// ここで宛先toLooperでしたい処理を記述

				return;
			}
		});
	}
}

呼び出し側はこんな感じかな。

new HogeTask().execute(toLooper, "hogehage");

toLooperには宛先スレッドのLooperを入れるとよい。Looperの取得の仕方は、対象となるスレッド上でLooper.myLooper()とすればよい。ちなみにUIスレッドのLooperを取得したい場合は、Looper.getMainLooper()を呼べばよい。

これで、HandlerとLooperの神髄を理解することができたと思う。これだけの知識があれば、スレッド間通信はお手の物のはずだ。