CountDownTimerを使ってはいけない2つの理由「Android」(1)
「Androidのアプリでカウントダウンタイマーを作りたい」そう思ったらどうやって実装すればよいのでしょうか。
ネットで検索してすぐ気がつくのは「CountDownTimer」というクラスがあり、それが名前の通り使えそうだということです。
こんにちはFeeeeelogです。
私もまずはCountDownTimerでタイマーを実装しようとしました。
結論から言います。
簡易的なカウントダウンタイマーとして使うにはCountDownTimerクラスは有効です。
しかし、Google playで公開するようなアプリの実装として使うのは危険です。
個人の範囲で利用するようなアプリの場合は問題ないでしょう。
それでは、具体的な2つの問題点について説明します。
1つ目の問題は「カウントダウンのタイミングに誤差がある」です。
スポンサーリンク
もくじ
CountDownTimerはカウントダウンのタイミングに誤差がある
CountDownTimer.javaの抜粋です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// handles counting down private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { synchronized (CountDownTimer.this) { final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime(); if (millisLeft <= 0) { onFinish(); } else if (millisLeft < mCountdownInterval) { // no tick, just delay until done sendMessageDelayed(obtainMessage(MSG), millisLeft); } else { long lastTickStart = SystemClock.elapsedRealtime(); onTick(millisLeft); // take into account user's onTick taking time to execute long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime(); // special case: user's onTick took more than interval to // complete, skip to next interval while (delay < 0) delay += mCountdownInterval; if (!mCancelled) { sendMessageDelayed(obtainMessage(MSG), delay); } } } } }; |
簡単に解説します。
CountDownTimerクラスは、カウントダウンする時間と間隔を指定して動作します。例えば、10分後に1秒間隔でカウントダウンするというような動作です。
ここで、1秒間隔でカウントダウンするとソースコードの”onTick(millisLeft)”が呼び出されます。millisLeftは残り時間(ミリ秒)です。
mCountDownIntervalは間隔(ミリ秒)のことで、1秒間隔であれば1000です。Handler#sendMessageDelayed(Message, long)の呼び出しによって第2引数に指定されたミリ秒後に、Handler#handleMessage(Message)を呼び出します。つまり、このソースのロジックをカウントダウンする回数繰り返します。
カウントダウンする間隔がずれても修正しない実装になっている
まず最初に言っておきますが、1秒を常に完璧に計測することはできません。1ミリ秒ずれるなどの可能性はあります。よって、ずれることが問題ではなく、ズレを修正する実装になっていないことが問題です。
long delayで遅延するミリ秒を計算しているのですが、具体的な例で説明します。
1 |
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime(); |
millisLeft = 4000, mStopTimeInFuture = 10000, SystemClock.elapsedRealtime() = 6000とします。
1 |
long lastTickStart = SystemClock.elapsedRealtime(); |
lastTickStart = 6000と同じ値だと仮定します。SystemClock.elapsedRealtime() = 6000
1 2 |
// take into account user's onTick taking time to execute long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime(); |
SystemClock.elapsedRealtime() = 6005で5ミリ秒進んだと仮定します。これはonTick(millisLeft)でUIの文字列を更新するなどの処理をするために時間が進みやすいからです。
long delay = 6000 + 1000 – 6005 = 995;
995ミリ秒後に、またHandler#handleMessage(Message)が呼び出されます。一間合っているように見えます。
995ミリ秒後、つまり残り3000ミリ秒になるからです。
しかし、Handler#sendMessageDelayed(Message, long)によって呼び出されるのは正確に995ミリ秒後にはならないことが多々あります。
Handler#handleMessage(Message)が呼び出される時のSystemClock.elapsedRealtime() = 7000が理想です。
しかし、現実はSystemClock.elapsedRealtime() = 7003 となることがあるでしょう。
再び動作してdelayを計算すると次のようになります。
long delay = 7003 + 1000 – 7007 = 996;
3ミリ秒ずれた時間を基準に1秒間隔を計算してしまいます。次のカウントはSystemClock.elapsedRealtime() = 8003を目指しています。
これを何度も繰り返すうちにonTick(millisLeft)が呼び出される間隔はずれていきます。
解決策1:妥協案
様々なブログにも書いている通り、1秒間隔でカウントダウンするならばそれより短い間隔(100~500ミリ程度)でカウントダウンすることでonTick(long)の呼び出す回数を増やしてごまかすということです。ミリ秒まで表示するカウントダウンタイマーを作れば分かりますが、実際には1秒単位で表示を更新するわけではありません。
カウントダウンのタイミングが重要でないならば問題ありません。そうでなければ厳密にはやはりずれていると言えるでしょう。
解決策2:本気で解決する
カウントダウンする間隔がずれても修正しない実装になっていると説明しました。つまり、ずれることが前提であり、そのずれ修正してカウントダウンするクラスを自作することです。CountDownTimer.javaを参考に自分でクラスを作ってください。
カウントダウン終了時刻にずれはない
1秒間隔のカウントダウンを期待しても、1秒からずれることは説明した通りです。ですが、カウントダウン終了時刻のonFinish()の呼び出しのずれはほとんどありません。ただし、次のページで説明する2つ目の問題が発生する場合はその限りではありません。
CountDownTimerの2つ目の問題
次の問題のほうが大きな問題です。
続きは「CountDownTimerを使ってはいけない2つの理由「Android」(2)」へ進んでください。