HandlerとMessage - 別スレッドでのGUI操作
ProgressDialogのサンプルプログラムをながめていたら、Handlerという見なれぬクラスが使われていた。
Handlerクラスて何だろう、と思って調べてみたらすっかりはまってしまった。
他の記事もそうなのだが、今回は特にサンプルコードをもとに推察で書いている部分が多いので、誤りがあればご容赦いただきたい。
動作確認は、きちんとしているつもりであるが。
別スレッドでのGUI操作の問題点
アンドロイドでも、通常のjavaプログラムのようにThreadクラスが使える。
しかし、アンドロイドのGUIはシングルスレッドにしか対応していないため、 ウィジェット等のGUIオブジェクトを生成したスレッドとは別のスレッドから、ウィジェットに直接アクセスする事はできない。
まず次のプログラムを実行して、別スレッドから直接ウィジェットの操作を試みてみる。
このプログラムを実行してボタンを押すと、新しいスレッドを作成して,GUIとは別スレッドから TextViewにアクセス(33行目)しようとするので、下図のようにエラーダイアログが表示されてしまう。
Handlerクラスのpostメソッドを使う
上記の問題を解決するには、Handlerクラスのpostメソッドを使う。
以下のプログラムのように、ウィジェットの属するスレッドにてHandlerクラスのインスタンスを生成して、このインスタンスの postメソッドの引数に、ウィジェットにアクセスするコードを記述したRunnableクラスを指定する。
今度は、ボタンを押してもエラーは発生しない。
これは、Handlerクラスのインスタンスのpostメソッドで指定したRunnableクラスのコードは、 Handlerクラスのインスタンスの属する(つまりウィジェットの属する)スレッドで実行される事による。
上記のコードの、onClickメソッド内を以下のように書き換えて、GUIとは別のスレッドでHandlerオブジェクトを生成しても効果はないので注意。
sendEmptyMessageメソッドを使ってhandleMessageメソッドを呼び出す
Handlerクラスを使ったもう一つの解決策として、 HandlerクラスのhandleMessageメソッドをオーバライドして、このメソッドを呼び出す方法がある。
以下にその例を示す。
この方法では、Handlerクラスを継承したクラスのインスタンスを作成して、 handleMessageメソッドをオーバライドする。
そして、このメソッドの中にGUI操作をおこなうコードを記述する。
別スレッドでsendEmptyMessageメソッドを実行すると、handler変数の属するスレッドでhandleMessageメソッドが実行される事になる。
postメソッドとsendEmptyMessageメソッドとの違いは、postメソッドでは引数に指定するRunnableオブジェクトのrunメソッドを実行するのに対して、 sendEmptyMessageメソッドではhandleMessageメソッドを呼び出す。
また、この方法ではhandleMessageメソッドに呼び出し側の情報を渡す事ができる。
上記コードの29行目では、コードを少し変更してTextViewに41行目のsendEmptyMessageの引数に指定した値を表示している。
sendEmptyMessageメソッドの引数は、Messageオブジェクトのwhatメンバーとして渡される。
Messageオブジェクトについては、次の項で説明する。
sendMessageメソッドを使ってMessageオブジェクトを渡す
sendEmptyMessageメソッドではなく、sendMessageメソッドを使ってhandleMessageメソッドを呼び出す事もできる。
このsendMessageメソッドを使うと、引数に指定するMessageオブジェクトを使ってより、多くの情報をhandleMessageメソッドに渡す事ができる。
以下にsendMessageメソッドの例を示す。
Messageオブジェクトは、publicフィールドとして以下の変数を持つ。
- int what
メッセージの識別子として使う事を目的とした、ユーザが勝手に定義して使う事のできる値。 - int arg1,int arg2
whatの他に、arg1とarg2の2つの整数値を渡す事ができる。 - Object obj
オブジェクトを渡したい場合には、この変数に代入すればよい。 - Messenger replyTo
正直なところ使い方はよくわからない、とりあえず無視して問題無いようだ。
この変数に値をセットして、sendMessageメソッドに渡す事によりhandleMessageメソッドで、値を受け取る事ができる。
以下に、このプログラムを実行してボタンをクリックした後の、ログ出力を示す。
また、MessageオブジェクトはgetDataメソッドとgetDataメソッドを使って、Bundleクラスのオブジェクトを受け渡しする事もできる。
Bundleオブジェクトは、ActivityクラスのonCreateメソッドの引数として画面遷移時の情報を受け渡す時にも使われているが、 簡単に言えばHashMapのようなものと考えれば良い。
キーとなる文字列を指定して、データを受け渡しする事ができる。
以下に、その例を示す。
このプログラムを実行して、ボタンを押した後の画面を以下に示す。
obtainMessageメソッドを使ってMessageオブジェクトを取得する。
グローバル・メッセージ・プールなるものがあって、MessageオブジェクトはobtainMessageメソッドを使って、このグローバル・メッセージ・プールから取得した方が効率が良いらしい。
obtainMessageメソッドは、以下のような引数をもつオーバーロードされたメソッドとして実装されている。
- obtainMessage()
- obtainMessage(int what)
- obtainMessage(int what, Object obj)
- obtainMessage(int what, int arg1, int arg2, Object obj)
- obtainMessage(int what, int arg1, int arg2)
これらのメソッドを使う事で、Messageオブジェクトの取得とデータの設定とを、同時におこなう事ができる。
少し前の例のMessageオブジェクトを生成してデータを設定するコードを、 obtainMessageメソッドを使って書き換えてみる。
実行時間をずらす
postメソッド,sendEmptyMessageメソッド,sendMessageメソッドには、 それぞれ以下のような、コードの実行を遅らせるDelayedメソッド, 指定時間になったらコードを実行するAtTimeメソッドが存在する。
実行を遅らせるメソッド。
- postDelayed(Runnable r, long delayMillis)
- sendEmptyMessageDelayed(int what, long delayMillis)
- sendMessageDelayed(Message msg, long delayMillis)
指定時間になったら実行するメソッド。
- postAtTime(Runnable r, long uptimeMillis)
- sendEmptyMessageAtTime(int what, long uptimeMillis)
- sendMessageAtTime(Message msg, long uptimeMillis)
以下に、sendMessageAtTimeメソッドの例を示す。
49行目のSystemClock.uptimeMillisメソッドは、ブートしてからの時間を返すメソッドである。
このプログラムを実行して、ボタンを押した時のログ出力を以下に示す。
若干、誤差があるもののresultMillisとuptimeMillisの差がdelayMillisとなっていることが確認できる。
参考URL
Handlerクラス内部のメカニズムについては throw Life - AndroidのHandlerとは何か?が参考になる。
Handlerクラスと Messageクラスのリファレンスも参照。