今回は定期実行に便利なタイマーです。
ストップウォッチを例題にTimer処理のポイントを3つ、紹介します。
- マルチスレッド処理について
- TimerやTimerTaskはcancelメソッド実行後は再利用できない
- Androidの描画手順(UI Threadを使うシングルスレッドモデル)
Androidでもjava.util.Timerが利用可能です。
ご存じの通り、Timerは新しいタスク(スレッド/Thread)を作成して、指定した遅延時間がたつと実行されます。
AndroidでTimerを使う際は、とくにスレッド処理について意識する必要があります。
経験上、タイマーを使う際は処理のついでに描画を更新したいときが多いのですが、Activityの描画ロジックがシングルスレッド前提で設計されているためです。
最初に1.スレッドについて紹介します。次に、ストップウォッチを例に2.タイマー処理を、
最後に3.Androidの描画モデルについて解説します。
Timerクラスの主要メソッド
メソッド(コンストラクタ) | 概要 |
---|---|
Timer(boolean isDaemon) | コンストラクタ。スレッド種別を指定 |
cancel() | タスクを破棄して終了 |
schedule(TimerTask task, long delay) | delayミリ秒あとに1度だけタスクを実行 |
schedule(TimerTask task, long delay, long period) | 繰返用。delayミリ秒あとにperiodミリ秒間隔でタスク実行 |
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | firstTime時間を基準としてperiodミリ秒単位でタスクを繰り返す |
scheduleAtFixedRate(TimerTask task, long delay, long period) | delayミリ秒あとの時間を基準としてperiodミリ秒単位でタスクを繰り返す |
以下、詳細です
1.スレッドとは
スレッドとは処理の単位のことです。AndroidのActivityに関する処理は通常mainスレッドで動作しています。
onCreateやOnPauseメソッド、各UIパーツ(ボタン、Viewなど)の描画処理などなどはmainスレッドで動いています。
実際に確認するにはEclipseのデバッグ・パーステクティブ内デバッグウインドウをのぞいてみてください。
画像はサンプルコードのストップウォッチ(停止状態)です。
Activity名[Android アプリケーション] L DalvikVM[localhost:xxxx] L スレッド[<1> main](実行中) L スレッド[<6> Binder Thread #2](実行中) L スレッド[<5> Binder Thread #1](実行中)
上記の例ではスレッド<1>がmainスレッドでActivityはシングルスレッドの状態だということがわかります。
(Binderはまた、別の機会に紹介したいと思います)
デーモンスレッドとユーザスレッド
スレッドの種類は2つに分類されます。
デーモンスレッドとは、プログラム終了時にスレッドの実行終了を待ちません。
プログラム終了のタイミングでデーモンスレッドの処理は中断され、終わることになります。
生成したスレッドで終了処理を意識しないですむので、Timer処理では使い勝手がよいです。
ユーザスレッドは、デーモンスレッドの反対でプログラムを終わるときに、スレッドの実行終了を待ちます。
プログラムを終了しようとしても、ユーザスレッドの処理が終わる(returnされる)まで終了できません。
(ユーザスレッドが生き残っている間は、プログラム実行状態です)
さきほどでてきた、mainスレッドはユーザスレッドです。
プログラム終了にあたっては、すべてのユーザスレッドできちんと終了処理がハンドリングできていないといけません。
利点として、処理が中断してしまう恐れはないので紛失できない大事な処理などに適しています。
Timer(boolean isDaemon);
上記はTimerのコンストラクタのうちの1つです。
引数をtrueで与えると、デーモンスレッドとして実行できます。
2.タイマー処理
今回のサンプルコードは、ストップウォッチです。サンプルコード(下図)は、経過時間、スタートボタン・ストップボタンで構成されています。
まず、はじめにストップウォッチのStartボタン、Stopボタン、テキスト表示領域を組み立てます。
TextView mTextView; Button mStartBtn, mStopBtn; float mLaptime = 0.0f; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //IDの取得 mTextView = (TextView) findViewById(R.id.LapTime); mStartBtn = (Button) findViewById(R.id.StartBtn); mStopBtn = (Button) findViewById(R.id.StopBtn); mStartBtn.setOnClickListener(this); mStopBtn.setOnClickListener(this); }
01~03行目、TextViewやButton、経過時間を入れる変数mLaptimeをActivityのメンバとして保持します。
onCreateメソッドでボタンのonClickListenerを登録します。
public class timerActivity extends Activity implements OnClickListener{ … (省略) … public void onClick(View v) { Button btn = (Button)v; switch( btn.getId() ){ //スタートボタンが押されたとき case R.id.StartBtn: if(mTimer == null){ //タイマーの初期化処理 timerTask = new MyTimerTask(); mLaptime = 0.0f; mTimer = new Timer(true); mTimer.schedule( timerTask, 100, 100); } break; //ストップボタンが押されたとき case R.id.StopBtn: if(mTimer != null){ mTimer.cancel(); mTimer = null; } break; default: break; } }
17~20行目でタイマーの初期化処理を、26~29行目でタイマーの終了処理を行っています。
タイマーの設定は次の設定の通り、事前にメンバとして初期化しています。
(コードが少し多いですが、もう一息です)
MyTimerTask timerTask = null; Timer mTimer = null; Handler mHandler = new Handler();
timerTask はclass MyTimerTask extends TimerTaskと、クラス内クラスとして宣言しています。
(ほんとうはクラス内クラスなんか使わない、もっと簡単な書き方があります。記事のいちばん最後に記載しました)
今回のストップウォッチでは、timerをデーモン・スレッドとして生成して、100ms間隔でタスク実行しています。
Timer関連の処理を抜き出してみましょう
//タイマーの初期化処理 timerTask = new MyTimerTask(); mLaptime = 0.0f; mTimer = new Timer(true); mTimer.schedule( timerTask, 100, 100); (省略) //タイマーの停止処理 mTimer.cancel(); mTimer = null;
Timerクラスのメソッドの意味はそれぞれ以下の表組みの通りです。
Timerクラスの主要メソッド
メソッド(コンストラクタ) | 概要 |
---|---|
Timer(boolean isDaemon) | コンストラクタ。スレッド種別を指定 |
cancel() | タスクを破棄して終了 |
schedule(TimerTask task, long delay) | delayミリ秒あとに1度だけタスクを実行 |
schedule(TimerTask task, long delay, long period) | 繰返用。delayミリ秒あとにperiodミリ秒間隔でタスク実行 |
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) | firstTime時間を基準としてperiodミリ秒単位でタスクを繰り返す |
scheduleAtFixedRate(TimerTask task, long delay, long period) | delayミリ秒あとの時間を基準としてperiodミリ秒単位でタスクを繰り返す |
scheduleとscheduleAtFixedRateの違い
scheduleとscheduleAtFixedRateの違いは繰り返し時間の基準点です。
schedule(TimerTask task, Date firstTime, long period) scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
scheduleメソッド:前回のタスク実行後を基準にperiodミリ秒後が次のタイミング
scheduleAtFixedRateメソッド:初回のタスク実行時間を基準にperiodミリ秒 x n回後が次のタイミング
(n回は繰り返し回数の例)
実はストップウォッチなので、scheduleメソッドを使わずに、
scheduleAtFixedRateメソッドを使った方が厳密でよかった、ということがわかります。
TimerやTimerTaskは再利用できない
ボタン押下時の初期化処理で毎回timerTask = new MyTimerTask、mTimer = new Timer(true)と、
インスタンスを新しく作り直しているのは、cancel()メソッド実行後はTimerクラスの資源が破棄されていて、
再利用できないからです。
よくハマるポイントなので注意してください。
複数スレッドを使う際の注意
Androidでの描画はUI Threadとよばれるスレッド(mainスレッドのこと)で処理される前提で、
シングルスレッドモデルと呼ばれています。
より詳細な情報については、Android DevelopersのBlog(http://android-developers.blogspot.com/2009/05/painless-threading.html)で触れられていますのでここでは肝となる要点だけ紹介します。
次のサンプルコードが、Timerでの更新処理です。
(サンプルでは、mTimer.schedule( timerTask, 100, 100);として100ms単位で実行)
class MyTimerTask extends TimerTask{ @Override public void run() { // mHandlerを通じてUI Threadへ処理をキューイング mHandler.post( new Runnable() { public void run() { //実行間隔分を加算処理 mLaptime += 0.1d; //計算にゆらぎがあるので小数点第1位で丸める BigDecimal bi = new BigDecimal(mLaptime); float outputValue = bi.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue(); //現在のLapTime mTextView.setText(Float.toString(outputValue)); } }); } } MyTimerTask timerTask = null; //onClickメソッドでインスタンス生成 Timer mTimer = null; //onClickメソッドでインスタンス生成 Handler mHandler = new Handler(); //UI Threadへのpost用ハンドラ
4行目のrun()メソッド内部が、更新処理です。
タイマーは別スレッドで実行することを思い出してください。
16行目のmTextView.setText();は描画の更新処理を含むため、UI Thread以外で呼び出すとシングルスレッドモデルに反します。
(例外で終了してしまうはずです)
強制終了を防いでいるのが、6行目mHandler.post();です。
Handlerは、シングルスレッドモデルを守るための仕組みで、UI Thread(mainスレッド)に対して描画処理をポストしています。
(実際には描画に関わるmTextView.setText() の部分だけpostすれば良いのですが、ここでは簡単化のため、すべての処理をUI Threadにお願いしました)
Handlerについては、adamrockerさんのWebサイト、throw Lifeにわかりやすい解説があります。
AndroidのHandlerとは何か?
http://www.adamrocker.com/blog/261/what-is-the-handler-in-android.html
MyTimerTaskなんてめんどくさいよ
今回は解説のためにTimerTaskの部分を別クラスに分けましたが、実際は
mTimer.schedule( timerTask, 100, 100);
など上記のコードを使わずに、以下のようにTimerTaskインスタンスを直接生成する場合が多いです。
mTimer.schedule( new TimerTask(){ @Override public void run() { // mHandlerを通じてUI Threadへ処理をキューイング mHandler.post( new Runnable() { public void run() { //実行間隔分を加算処理 mLaptime += 0.1d; //計算にゆらぎがあるので小数点第1位で丸める BigDecimal bi = new BigDecimal(mLaptime); float outputValue = bi.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue(); //現在のLapTime mTextView.setText(Float.toString(outputValue)); } }); } }, 100, 100);
コードとしては若干見にくいかもしれませんが、MyTimerTaskを作らずに記述できます。
両方の記法を覚えておけば便利です(どちらが良いかは用途、意図に依存するとおもいます)。