2012年7月25日水曜日

HandlerとMessage - 別スレッドでのGUI操作

ProgressDialogのサンプルプログラムをながめていたら、Handlerという見なれぬクラスが使われていた。
Handlerクラスて何だろう、と思って調べてみたらすっかりはまってしまった。

アンドロイドでも、通常のjavaプログラムのようにThreadクラスが使える。
しかし、アンドロイドのGUIはシングルスレッドにしか対応していないため、 ウィジェット等のGUIオブジェクトを生成したスレッドとは別のスレッドから、ウィジェットに直接アクセスする事はできない。

まず次のプログラムを実行して、別スレッドから直接ウィジェットの操作を試みてみる。

01
package gudon.sample.handler1;
02

03
import android.app.Activity;
04
import android.os.Bundle;
05
import android.view.View;
06
import android.widget.Button;
07
import android.widget.LinearLayout;
08
import android.widget.TextView;
09

10
public class HandlerSample1 extends Activity {
11

12
@Override
13
public void onCreate(Bundle savedInstanceState) {
14
super.onCreate(savedInstanceState);
15

16
LinearLayout layout = new LinearLayout(this);
17
layout.setOrientation(LinearLayout.VERTICAL);
18

19
setContentView(layout);
20

21
final TextView tv=new TextView(this);
22
tv.setText("hoge hoge");
23
layout.addView(tv);
24

25
Button button = new Button(this);
26
button.setText("ボタンをおしてください");
27
button.setOnClickListener(new View.OnClickListener() {
28
public void onClick(View v) {
29
new Thread(new Runnable() {
30

31
@Override
32
public void run() {
33
tv.setText("別スレッドよりGUIにアクセスします。");
34
}
35
}).start();
36
}
37
});
38
layout.addView(button);
39
}
40
}
このプログラムを実行してボタンを押すと、新しいスレッドを作成して,GUIとは別スレッドから TextViewにアクセス(33行目)しようとするので、下図のようにエラー
ダイアログが表示されてしまう。



Handlerクラスのpostメソッドを使う
上記の問題を解決するには、Handlerクラスのpostメソッドを使う。
以下のプログラムのように、ウィジェットの属するスレッドにてHandlerクラスのインスタンスを生成して、このインスタンスの postメソッドの引数に、ウィジェットにアクセスするコー
ドを記述したRunnableクラスを指定する。
01
package gudon.sample.handler1;
02

03
import android.app.Activity;
04
import android.os.Bundle;
05
import android.os.Handler;
06
import android.view.View;
07
import android.widget.Button;
08
import android.widget.LinearLayout;
09
import android.widget.TextView;
10

11
public class HandlerSample1 extends Activity {
12

13
@Override
14
public void onCreate(Bundle savedInstanceState) {
15
super.onCreate(savedInstanceState);
16

17
LinearLayout layout = new LinearLayout(this);
18
layout.setOrientation(LinearLayout.VERTICAL);
19

20
setContentView(layout);
21

22
final TextView tv=new TextView(this);
23
tv.setText("hoge hoge");
24
layout.addView(tv);
25

26
final Handler handler=new Handler();
27

28
Button button = new Button(this);
29
button.setText("ボタンをおしてください");
30
button.setOnClickListener(new View.OnClickListener() {
31
public void onClick(View v) {
32
new Thread(new Runnable() {
33

34
@Override
35
public void run() {
36
handler.post(new Runnable() {
37

38
@Override
39
public void run() {
40
tv.setText("別スレッドよりGUIにアクセスします。(改善版)");
41
}
42
});
43
}
44
}).start();
45
}
46
});
47
layout.addView(button);
48
}
49
}
今度は、ボタンを押してもエラーは発生しない。
これは、Handlerクラスのインスタンスのpostメソッドで指定したRunnableクラスのコードは、 Handlerクラスのインスタンスの属する(つまりウィジェットの属する)ス
レッドで実行される事による。
上記のコードの、onClickメソッド内を以下のように書き換えて、GUIとは別のスレッドでHandlerオブジェクトを生成しても効果はないので注意。
31
public void onClick(View v) {
32
new Thread(new Runnable() {
33

34
@Override
35
public void run() {
36
Handler handler=new Handler();
37
handler.post(new Runnable() {
38

39
@Override
40
public void run() {
41
tv.setText("別スレッドよりGUIにアクセスします。");
42
}
43
});
44
}
45
}).start();
46
}
sendEmptyMessageメソッドを使ってhandleMessageメソッドを呼び出す
Handlerクラスを使ったもう一つの解決策として、 HandlerクラスのhandleMessageメソッドをオーバライドして、このメソッドを呼び出す方法がある。
以下にその例を示す。
01
package gudon.sample.handler1;
02

03
import android.app.Activity;
04
import android.os.Bundle;
05
import android.os.Handler;
06
import android.os.Message;
07
import android.view.View;
08
import android.widget.Button;
09
import android.widget.LinearLayout;
10
import android.widget.TextView;
11

12
public class HandlerSample1 extends Activity {
13

14
@Override
15
public void onCreate(Bundle savedInstanceState) {
16
super.onCreate(savedInstanceState);
17

18
LinearLayout layout = new LinearLayout(this);
19
layout.setOrientation(LinearLayout.VERTICAL);
20

21
setContentView(layout);
22

23
final TextView tv=new TextView(this);
24
tv.setText("hoge hoge");
25
layout.addView(tv);
26

27
final Handler handler=new Handler() {
28
public void handleMessage(Message msg) {
29
tv.setText(String.format("what=%d",msg.what));
30
}
31
};
32

33
Button button = new Button(this);
34
button.setText("ボタンをおしてください");
35
button.setOnClickListener(new View.OnClickListener() {
36
public void onClick(View v) {
37
new Thread(new Runnable() {
38

39
@Override
40
public void run() {
41
handler.sendEmptyMessage(0);
42
}
43
}).start();
44
}
45
});
46
layout.addView(button);
47
}
48
}
この方法では、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メソッドの例を示す。
01
package gudon.sample.handler1;
02

03
import android.app.Activity;
04
import android.os.Bundle;
05
import android.os.Handler;
06
import android.os.Message;
07
import android.util.Log;
08
import android.view.View;
09
import android.widget.Button;
10
import android.widget.LinearLayout;
11
import android.widget.TextView;
12

13
public class HandlerSample1 extends Activity {
14
private static final String TAG = "HandlerSample1";
15

16
@Override
17
public void onCreate(Bundle savedInstanceState) {
18
super.onCreate(savedInstanceState);
19

20
LinearLayout layout = new LinearLayout(this);
21
layout.setOrientation(LinearLayout.VERTICAL);
22

23
setContentView(layout);
24

25
final TextView tv=new TextView(this);
26
tv.setText("hoge hoge");
27
layout.addView(tv);
28

29
final Handler handler=new Handler() {
30
public void handleMessage(Message msg) {
31
tv.setText(String.format("what=%d",msg.what));
32
Log.v(TAG,String.format("what=%s\targ1=%d\targ2=%d\t%s",
33
msg.what,msg.arg1,msg.arg2,msg.obj));
34
}
35
};
36

37
Button button = new Button(this);
38
button.setText("ボタンをおしてください");
39
button.setOnClickListener(new View.OnClickListener() {
40
public void onClick(View v) {
41
new Thread(new Runnable() {
42

43
@Override
44
public void run() {
45
Message msg = new Message();
46
msg.what=99;
47
msg.arg1=1;
48
msg.arg1=2;
49
msg.obj="obj";
50

51
handler.sendMessage(msg);
52
}
53
}).start();
54
}
55
});
56
layout.addView(button);
57
}
58
}
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のようなものと考えれば良い。
キーとなる文字列を指定して、データを受け渡しする事ができる。
以下に、その例を示す。
01
final Handler handler=new Handler() {
02
public void handleMessage(Message msg) {
03
Bundle data=msg.getData();
04
tv.setText(String.format("text=%s , number=%d",
05
data.getString("text"), data.getInt("number")));
06
}
07
};
08

09
Button button = new Button(this);
10
button.setText("ボタンをおしてください");
11
button.setOnClickListener(new View.OnClickListener() {
12
public void onClick(View v) {
13
new Thread(new Runnable() {
14

15
@Override
16
public void run() {
17
Message msg = new Message();
18

19
Bundle data=new Bundle();
20
data.putString("text", "any string");
21
data.putInt("number", 123);
22
msg.setData(data);
23

24
handler.sendMessage(msg);
25
}
26
}).start();
27
}
28
});
29
layout.addView(button);
このプログラムを実行して、ボタンを押した後の画面を以下に示す。



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メソッドを使って書き換えてみる。
43
public void run() {
44
Message msg = handler.obtainMessage(99,1,2,"obj");
45
handler.sendMessage(msg);
46
}
実行時間をずらす
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メソッドの例を示す。
01
package gudon.sample.handler1;
02

03
import android.app.Activity;
04
import android.os.Bundle;
05
import android.os.Handler;
06
import android.os.Message;
07
import android.os.SystemClock;
08
import android.util.Log;
09
import android.view.View;
10
import android.widget.Button;
11
import android.widget.LinearLayout;
12
import android.widget.TextView;
13

14
public class HandlerSample1 extends Activity {
15
private static final String TAG = "HandlerSample1";
16

17
@Override
18
public void onCreate(Bundle savedInstanceState) {
19
super.onCreate(savedInstanceState);
20

21
LinearLayout layout = new LinearLayout(this);
22
layout.setOrientation(LinearLayout.VERTICAL);
23

24
setContentView(layout);
25

26
final TextView tv=new TextView(this);
27
tv.setText("hoge hoge");
28
layout.addView(tv);
29

30
final Handler handler=new Handler() {
31
public void handleMessage(Message msg) {
32
Log.v(TAG,String.format("resultMillis=%d",SystemClock.uptimeMillis()));
33
Bundle bundle=msg.getData();
34
Log.v(TAG,String.format("uptimeMillis=%d",bundle.getLong("uptimeMillis")));
35
Log.v(TAG,String.format("delayMillis=%d",bundle.getLong("delayMillis")));
36
tv.setText(String.format("what=%d",msg.what));
37
}
38
};
39

40
Button button = new Button(this);
41
button.setText("ボタンをおしてください");
42
button.setOnClickListener(new View.OnClickListener() {
43
public void onClick(View v) {
44
new Thread(new Runnable() {
45

46
@Override
47
public void run() {
48
Message msg = handler.obtainMessage();
49
long uptimeMillis = SystemClock.uptimeMillis();
50
long delayMillis = 3000; // 3秒後
51

52
Bundle data=new Bundle();
53
data.putLong("uptimeMillis", uptimeMillis);
54
data.putLong("delayMillis", delayMillis);
55
msg.setData(data);
56

57
handler.sendMessageAtTime(msg,
uptimeMillis+delayMillis);
58
}
59
}).start();
60
}
61
});
62
layout.addView(button);
63
}
64
}
49行目のSystemClock.uptimeMillisメソッドは、ブートしてからの時間を返すメソッドである。
このプログラムを実行して、ボタンを押した時のログ出力を以下に示す。

若干、誤差があるもののresultMillisとuptimeMillisの差がdelayMillisとなっていることが確認できる。

0 件のコメント:

コメントを投稿