また唐突だがAndroidアプリ開発の記事を書く。
アプリのユーザビリティを考えると、マルチスレッド処理は必須だ。
Android SDKでマルチスレッド処理を書く場合、いろいろな方法がある。どんな方法があるかはググってもらった方が早いだろう。一応、一通り便利に処理するための枠組みは用意されている。まぁ、「スレッドの存在は意識してもらうけど管理はライブラリに任せてくれたまえ」くらいな感じ。いいところだと思う。
別スレッド処理が必要な典型例としては「バックグラウンドで重い処理」と「周期的な処理」が考えられる。
AndroidではUIコンポーネント(画面上のボタンなど)はメインスレッド(UIスレッド)からしかアクセスできないという仕様があるので、別スレッドからUIコンポーネントを操作するにはUIスレッドとスレッド間通信みたいなことをする必要が出てくる。例えば、「バックグラウンドで重いファイルをダウンロードしながらその進捗率をプログレスバーで表示する」という処理をするには、ワーカースレッドでダウンロード監視を行いつつ、状況をUIスレッドに通知してプログレスバーを進ませるという記述をしなければならない。実はそういった処理に関してはAsyncTaskという便利なクラスが用意されており、比較的簡単に、特別なことを考えなくても実装出来るようになっている。
さて、今回やりたかったのは「OpenGL描画のFPSをTextViewで定期的に表示する」という、後者(周期的な処理)のパターン。この実装もAsyncTaskで出来るかな、と思ったのだが、どうもよくよく調べてみるとAsyncTaskは明らかに「使い捨て一度きり!」な作りになっており、ちょっと周期的処理に使うには無理がある。ということで、ちゃんとAndroid的なマルチスレッド処理を書かなければならない。
最初は自前でThreadを作って回そうかと思ったが、幸いなことにAndroid SDKにはjava.util.Timerクラスとjava.util.TimerTaskクラスが含まれているのでこれを利用することにする。あとはUIスレッドと通信するためのandroid.os.Handlerクラス(こいつについてはthrow Lifeさんの「AndroidのHandlerとは何か?」が大変参考になる)。
はてさて、前置きが長くなったが、どうせ周期部分の処理なんて同じパターンになるだろうので後でも使い回せそうなクラスを作ってみたという話である。以下、クラス定義をベタ貼り。
/**
* 周期的に何か処理を走らせるためのクラス。
* 周期カウントはこのクラスのインスタンスで作るスレッドで行われる。
* invokersMethod()の中の処理はこのインスタンスを作成したスレッドで行われる。
* 継承して使ってください。
*/
public abstract class AbstractPeriodicTask {
private long period;
private boolean isDaemon;
private boolean isCancelled = true;
private Timer timer;
private TimerTask timerTask;
private Handler handler;
/**
* periodミリ秒の周期で動かす
* @param period 周期ミリ秒
* @param isDaemon 定期処理を行うスレッドをデーモンスレッドで作成するかどうか(false=ユーザースレッド)
*/
public AbstractPeriodicTask(long period, boolean isDaemon) {
handler = new Handler();
this.period = period;
this.isDaemon = isDaemon;
}
/**
* periodミリ秒の周期で動かす。タイマースレッドはユーザースレッドで作成される
* @param period 周期ミリ秒
*/
public AbstractPeriodicTask(long period){
this(period,false);
}
/**
* 周期タスクの実行を開始する
*/
public void execute(){
if(!isCancelled){
//isCancelledがfalse(=実行中)ならばこのメソッドは実行しない
return;
}
//timerをキャンセルした場合、timer,timerTaskは破棄されるので都度作り直す
timerTask = new TimerTask(){
@Override
public void run() {
preInvokersMethod();
handler.post(new Runnable(){
@Override
public void run() {
invokersMethod();
}
});
postInvokersMethod();
}
};
timer = new Timer(isDaemon);
timer.scheduleAtFixedRate(timerTask, period, period);
}
/**
* 周期タスクの実行をキャンセルする
*/
public void cancel(){
if(timer==null || timerTask==null){
return;
}
timer.cancel();
timer = null;
isCancelled = true;
}
/**
* 本インスタンスを作成したスレッド(例えばUIスレッド)で処理させるメソッド
*/
abstract protected void invokersMethod();
/**
* タイマースレッドで処理させるメソッドでinvokersMethodの直前に呼ばれる。
*/
protected void preInvokersMethod(){
}
/**
* タイマースレッドで処理させるメソッドでinvokersMethodの直後に呼ばれる。
*/
protected void postInvokersMethod(){
}
}
AsyncTaskの作りをなんとなく参考にして作ったが、大したことはしてません。TimerTaskを回す部分と、Handlerを作成して処理を元スレッドに依頼する部分をやるだけのクラス…。なんだかこうやって貼り付けると長いコードに感じてしまう。一応、中断も考慮してみたけども、これで本当に大丈夫だろうか…?まぁ問題があれば修正エントリを書けばいいか。
使用例として、1秒ごとにカウンターを進めるアプリを試作。こんな感じで使える。
/**
* 周期的にカウントを進めるクラス
*/
public class PeriodicCounter extends AbstractPeriodicTask {
TextView countView;
private int count;
public PeriodicCounter(long period, boolean isDaemon, TextView countView) {
super(period, isDaemon);
this.countView = countView;
}
@Override
protected void invokersMethod() {
count++;
countView.setText(Integer.toString(count));
}
}
/**
* 言わずもがなMainActivityクラス
*/
public class PeriodicExp extends Activity {
private TextView countDisplay;
private PeriodicCounter counter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//フルスクリーン表示
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
//タイトルバー非表示
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.main);
countDisplay = (TextView) findViewById(R.id.countDisplay);
//カウンターを周期1000ms、カウント表示をcountDisplayで生成
counter = new PeriodicCounter(1000,true, countDisplay);
}
@Override
protected void onResume() {
super.onResume();
if(counter!=null){
counter.execute();
}
}
@Override
protected void onPause() {
if(counter!=null){
counter.cancel();
}
super.onPause();
}
}
PeriodicTaskを継承したPeriodicCounterクラスを定義。そのインスタンスで周期カウント→都度UIスレッドでカウントを描画、というだけのアプリ。長くなったのでmain.xmlは書かなくていいだろう(countDisplayというIDのTextViewがあるだけ)。
とりあえずこれで周期的に何かやる、みたいのは楽になったと思う。
ちょっとこれで別に試したいこともでてきたけど、それはまた別件で。
(2011/05/25) ソース修正。
PeriodicTaskクラスをAbstract化してみた。→AbstractPeriodicTaskクラス。
これで典型的なTemplateMethodパターンっぽくなった?