Androidで学ぶ並列処理とGUI
Docomo製Android端末の発売日が発表されました。来月の札幌JavaコミュニティもAndroidイベントということもあり、チュートリアルを作成したりしています。ちょっとスレッド周りではまった事もあり、簡単な時計アプリケーションの作り方から、Androidのスレッド描画モデルを紹介します。
とりあえずソース
このアプリケーションは、0.5秒毎に日時を更新する単純な時計アプリケーションです。
package com.example.android; // import 略 public class Clock extends Activity { private ScheduledExecutorService service; private Handler handler = new Handler(); /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final TextView textView = new TextView(this); textView.setText(new Date().toString()); setContentView(textView); service = Executors.newSingleThreadScheduledExecutor(); service.scheduleAtFixedRate(new Runnable() { @Override public void run() { handler.post(new Runnable() { @Override public void run() { textView.setText(new Date().toString()); } }); } }, 0, 500, TimeUnit.MILLISECONDS); } @Override protected void onDestroy() { super.onDestroy(); service.shutdown(); } }
ポイントは2つあり、java.util.concurrent.ScheduledExecutorService と android.os.Handlerです。
定期的に処理を実行する
java.util.concurrent.ScheduledExecutorServiceはJava5から導入されたconcurrent フレームワークに含まれるインターフェイスです。concurrent フレームワークは並列処理を強力にサポートするライブラリで、並列処理を考える上では必修なライブラリです。そのコンセプトは、処理をする人(スレッド)とその管理を実際に行う処理と分けることです。スレッドの生成やスレッドの管理に関しては特殊なことをやらない限りは似たような管理しか必要ありません。例えば1つのスレッドが順次処理をしたり、最大10個のスレッドが動きそれ以上の処理は待機するなどです。これに対し、処理はプログラムによってマチマチです。したがって、concurrentフレームワークでは、プログラマが処理をする部分に集中してコーディングできるように設計されています。
Executorsクラス
Executorsクラスはconcurrent フレームワークで提供されるユーティリティクラスで、よくあるような並列処理を行うサービスを作成します。今回は、定期的に処理を行いたいのでnewSingleThreadScheduledExecutorを使い、ScheduledExecutorServiceのインスタンスを取得しています。同時に色々と処理するわけではないので1個のスレッドで構いません。
ScheduledExecutorService
ScheduledExecutorServiceは定期的な実行や何時間後に実行などのAPIを提供するサービスです。今回はscheduleAtFixedRateを使用しています。このメソッドは第1引数にRunnableの実装クラスを与え、残りの引数でどのくらいの期間で実行するかを指定します。ソースでは0秒のディレイをとって、500ミリ秒毎に繰り返し実行するような指定になっています。
他にもあくまで処理間隔を指定して実行させるscheduleWithFixedDelayや、指定時間後に1回だけ実行するscheduleメソッドもあります。
GUIの更新を行うスレッド
最近のGUIは全てシングルスレッドモデルでデザインされています。つまり、GUIの更新などを行うスレッドは1つしかないのです。これはAndroidも同様です。これは下手にマルチスレッドでGUIを更新すると設計も複雑になり、使う方も難しくなるためです。
したがって、Androidのコードを書く場合、そのGUIを更新するような処理がどのスレッドで実行されているかを意識しなくてはなりません。とはいえ、自分でスレッドを起こさない限りは全ての処理は同じスレッドで行われます。問題はスレッドを自分で作成した場合で、その中で行われる処理にGUIの描画に関連する処理は絶対に含めてはなりません*1。
Androidではこのような事を実現するために、 android.os.Handlerというクラスが用意されています。このクラスは内部的にイベントのキューを持っており、キューに描画イベントを溜めておき、「後で」GUIスレッドに実行させます。具体的にはHandlerのpostメソッドです。HandlerのpostメソッドはRunnableインターフェイスを受け取り、キューに溜めます。したがって、Handlerにpostするだけならば自作スレッドで行っても構わないのです。
自作スレッドでビューを更新するとデットロックする
Swingでは自作スレッドでsetTextなんかを呼んでしまってもとりあえず動いています。本当はダメなんですが、ある程度は動くのでサンプルなどでは意識していないケースもあります。ですが、Androidでは自作スレッドでsetTextなんかを呼ぶとほぼ確実にロックしますので注意してください。
service.scheduleAtFixedRate(new Runnable() { // あっという間にロックしてしまいます。 textView.setText(new Date().toString()); }, 0, 500, TimeUnit.MILLISECONDS);
Handlerのスコープに注意
Handlerは複数作ることも可能です。最終的に管理されるキューは1つしかありませんが、Handlerのコンストラクタの中でシングルトンなLooperインスタンスからキューを取得しているため、問題になりません。しかし、次のように書いてしまうとやはりロックします。
service.scheduleAtFixedRate(new Runnable() { @Override public void run() { new Handler().post(new Runnable() { @Override public void run() { textView.setText(new Date().toString()); } }); } }, 0, 500, TimeUnit.MILLISECONDS);
一見、別スレッドからですが、描画スレッドへの依頼のためにHandlerを使い処理をキューイングしています。登録されるキューは1つなので上手く動くはずですが、これもロックします。詳しくは調査していませんが、Handlerの参照の問題なのかもしれません。解決する為には、最初のサンプルのようにHandlerをActibityのフィールドにします。
これで自分の作成したスレッドが定期的に描画依頼を行い、描画依頼を受けたGUIスレッドが新しい時刻へテキストを変更しているようになりました。単純な例なので委譲・委譲と面倒なことになっていますが、GUIを作る上ではこれが一番シンプルなのです。
Messageオブジェクトによるのキューイング
HandlerはRunnableのキューを受け付けるだけでなく、Messageオブジェクトによるのキューイングも可能です。
private ScheduledExecutorService service; private Handler handler; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final TextView textView = new TextView(this); textView.setText(new Date().toString()); setContentView(textView); handler = new Handler() { public void handleMessage(android.os.Message msg) { textView.setText((String) msg.obj); }; }; service = Executors.newSingleThreadScheduledExecutor(); service.scheduleAtFixedRate(new Runnable() { @Override public void run() { Message msg = new Message(); msg.obj = new Date().toString(); handler.sendMessage(msg); } }, 0, 500, TimeUnit.MILLISECONDS); }
注意して欲しいのは、他のスレッドからキューイングするのはsendMessageで、それを処理するのはhandleMessageだということです。送るメッセージと実際に描画する内容がまったく離れるような場合は有効ですが、単純なアプリケーションではRunnableでpostした方が解りやすいでしょう。分離する意味があれば検討してください。
後始末はつけること
Executorはスレッドに関するライフサイクルをすべて任せることができるわけですが、もう使わなくなったならば後始末する必要があります。そうしないと、そのスレッドは永遠に彷徨い続けてしまいます。
@Override protected void onDestroy() { super.onDestroy(); service.shutdown(); }
*1:これはSwingも同様です