2011年6月7日火曜日

AndroidのHandlerとLooper

前置き
本稿では、AndroidHandlerクラスやLooperクラスを取り上げます。これらは、Androidでイベントドリブンのプログラムを書くときに欠かせないものです。
本文
イベントドリブン
死語でしょうか。もしかしたら最も古いデザインパターンの一つかもしれません。以下のような特徴を持ちます。
  • あるスレッドが、無限ループを持つ
  • ループでは、メッセージを待ち、メッセージが来たらそれを処理する
  • 送られたメッセージはキューに蓄積され、届いた順に処理される
この無限ループのことを「メッセージループ」、メッセージを処理するコードのことを「メッセージハンドラ(あるいは単にハンドラ)」、キューのことを「メッセージキュー」と呼びます。また「メッセージ」のことは「イベント」と呼ぶ場合もあります。
イベントドリブンで良く見かける問題
例えば「ボタンがタッチされた」というメッセージに対するハンドラを書いているとしましょう。次のようなシナリオを想像して下さい。
  • このハンドラの中では、タッチされたアイコンを大きくしてから効果音を鳴らす
  • アイコンを大きくする処理は非同期処理である(つまりアイコンを大きくしても、すぐに画面が更新されるわけではなく、再描画メッセージがキューに送られるだけ)
  • 効果音は同期処理ですぐに鳴る
ここで、画面が更新されてから効果音が鳴るようにしたい場合は、「ボタンがタッチされた」というメッセージのハンドラの中で効果音を鳴らすわけにはいきません。一旦ハンドラを終わらせて、再描画メッセージが処理された後で効果音を鳴らす必要があります。そのため、「ボタンがタッチされた」というメッセージのハンドラの中では、自スレッド宛てに「効果音を鳴らせ」メッセージを送るだけにしておきます。
こういうケースが一般的に何と呼ばれているのか不明ですが、自分は「一旦ループに返す」とか「次の周回で処理する」とか言ってました。
Androidでのイベントドリブン
Androidには、イベントドリブンを実現するためのクラスが揃っています。Looperクラスがメッセージループに、Handlerクラスがメッセージハンドラに、Messageクラスがメッセージに、MessageQueueクラスがメッセージキューに、それぞれ相当します。
ActivityベースのAndroidアプリを作るとメインスレッド(別名UIスレッド)が自動的に生成されますが、このスレッドはイベントドリブン構造になっており、ユーザ操作(キーやタッチ)や再描画要求といったメッセージを処理します。なので、例えばボタンをタッチしたところでブレークをかけ、コールスタックを見ると、Looper#loop()を通っていることが確認できます。
Looperクラス
Looperクラスはメインスレッドだけのものではありません。独自スレッド内でイベントドリブンしたい場合もLooperクラスを使うことができます。
new Thread(new Runnable() { public void run() {
    Looper.prepare();
    Looper.loop();
}}).start();
prepare()で、LooperオブジェクトとMessageQueueオブジェクトが生成され、カレントスレッドと関連付けられます。loop()で、カレントスレッド上でメッセージループが回り始めます。どちらもクラスメソッドです。また、引数にスレッドを指定することはできません(つまり、常にカレントスレッドに対して作用するということです)
ただし、上記のコードでは誰もメッセージを送れないし、仮にメッセージが届いても何も処理されません。メッセージを送ったり処理するためにはHandlerオブジェクトが必要です。
Handlerクラス
メインスレッドや独自スレッドのLooper上で独自のメッセージを処理したい場合は、Handlerクラスを継承した独自クラスを作ります。さらにHandler#handleMessage()メソッドをオーバーライドし、その中で届いたメッセージを処理します。そうしておけば、Handler#sendMessage()を使ってMessageオブジェクトを送信することができます。結構、面倒くさいですね。
ただ、前述の「次の周回で処理する」くらいであれば、独自のメッセージを用意したりHandlerクラスを継承する必要はありません。Handlerには、Messageオブジェクトの他にもRunnableオブジェクトを送る機能があるのです。ここでのRunnableオブジェクトは、MessageオブジェクトとHandler#handleMessage()がセットになったようなものです。
public void onCreate(Bundle savedInstanceState) {
    ...
    handler = new Handler();
}
 
public void onClick(View arg0) {
    handler.post(new Runnable() { public void run() {
        // 次の周回で行いたい処理。
    }});
}
このように、あらかじめHandlerオブジェクトを生成しておき、そこへ、次の周回で行いたい処理を実装したRunnableオブジェクトをpost()するだけです。Handlerオブジェクトをfieldにせずに、一時オブジェクトにしてもOKかもしれません(ダメかもしれません)。生成したHandlerオブジェクトは、カレントスレッド(及び、そのLooperオブジェクトやMessageQueueオブジェクト)に対応付けられます。
Handlerコンストラクタの引数にはLooperオブジェクトを指定することもできるので、Handlerオブジェクトをカレントスレッド以外のスレッドに対応付けることが可能です(Looper生成時とは対照的ですね)
new Thread(new Runnable() { public void run() {
    // メインスレッド用のHandlerを生成。
    Handler mainHandler = new Handler(Looper.getMainLooper());
    mainHandler.post(new Runnable() { public void run() {
        ...
    }});
}}).start();
ただし、どのスレッドに対応付ける場合でも、そのスレッド上でLooper#prepare()しておく必要があります。
クラス関連
イベントドリブンに関係するクラスの関連図を示します。
Threadオブジェクト、Looperオブジェクト、及びMessageQueueオブジェクトは、それぞれ1つずつ。それに対してHandlerオブジェクトは複数個あっても構いません。また、Handlerオブジェクトを使って送ることができるのは、RunnableオブジェクトかMessageオブジェクトです。図では、Runnable用のHandlerMessage用のHandlerに分かれているように見えますが、そういう意図はありません(混在可能です)
各オブジェクト間の関連を取得するメソッドを挙げます。「C/I」のCはクラスメソッド、Iはインスタンスメソッドです。
クラス メソッド C/I 何を返すか
Looper myLooper() C カレントスレッドのLooper
Looper myQueue() C カレントスレッドのMessageQueue
Looper getMainLooper() C メインスレッドのLooper
Looper getThread() I 当該Looperのスレッド
Handler getLooper() I 当該HandlerのLooper
Handlerを取得するメソッドがありませんね。Handlerは自分で生成して覚えておくもの、ということでしょう。Looper#getThread()はあるのに、Looper#getQueue()が無いのは不便な気がします。
その他のクラス
MessageQueueクラスには、アイドル時に行いたい処理を登録する機能があります。
HandlerThreadクラスは、最初からLooper#prepare()されたThreadクラスです。名前に反して、Handlerオブジェクトに関する世話は焼いてくれません。またLooper#loop()のコールもプログラマの責任です。HandlerThread#getLooper()により、当該スレッドに対応付けられたLooperオブジェクトを得ることができます。
 
 

0 件のコメント:

コメントを投稿