Android 实现倒计时的方式有多种,Handler 延时发送 Message,Timer 和 TimerTask 配合使用,使用 CountDownTimer 类等。相比而言,经过系统封装的 CountDownTimer 算是使用起来最为方便的方式之一。
然而,CountDownTimer 有两个使用上的问题我们不得不稍加注意:计时不准确、内存泄漏问题。我们来结合源码逐一分析一下。
计时不准确问题
举个简单的例子,利用 CountDownTimer 实现一个时长为 5 秒的 View 倒计时显示,要求从 5s 开始每隔 1 秒倒计时显示到 1s:
CountDownTimer timer = new CountDownTimer(5000, 1000){
@Override
public void onTick(long millisUntilFinished) {
mSampleTv.setText(millisUntilFinished / 1000 + "s");
}
@Override
public void onFinish() {
}
};
timer.start();复制代码
理想状态下,TextView 按照我们想象的那样,从 "5s" 开始显示,然后 “4s”、“3s”,直到显示 “1s”。然而事实却是从 “4s” 开始显示的(例子很简单,此处不再放图)。
这说明在 onTick 回调方法中的参数有问题,那就在该方法中添加一句日志:
Log.d("onTick", millisUntilFinished + "");复制代码
打印结果如下:
09-26 22:09:01.429 22197-22197/com.yifeng.sample D/onTick: 4985
09-26 22:09:02.430 22197-22197/com.yifeng.sample D/onTick: 3984
09-26 22:09:03.432 22197-22197/com.yifeng.sample D/onTick: 2982
09-26 22:09:04.434 22197-22197/com.yifeng.sample D/onTick: 1981复制代码
可以看到,onTick 方法并不是从我们设定的 5000 毫秒开始倒计时的!
于是分析 CountDownTimer 类,查看 start() 方法的源码:
public synchronized final CountDownTimer start() {
mCancelled = false;
if (mMillisInFuture <= 0) {
onFinish();
return this;
}
mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
mHandler.sendMessage(mHandler.obtainMessage(MSG));
return this;
}复制代码
能够看出,CountDownTimer 在内部也是借助 Handler 实现的。同时,初始化 CountDownTimer 时的 millisInFuture 参数将转化成 mStopTimeInFuture 值。值得注意的是,在转化的同时,还自动添加这个时间:
SystemClock.elapsedRealtime()复制代码
这个表达式的时间值表示系统启动到当前程序代码执行时的时间毫秒数。先暂且不管,继续看这个 Handler 的实现:
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
synchronized (CountDownTimer.this) {
if (mCancelled) {
return;
}
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;
sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
};复制代码
可以看到,在计算倒计时剩余时间的地方,再次使用到当前代码执行时的时间值:
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();复制代码
也就是说,CountDownTimer 的内部实现比我们理想的计算更加精准,将 start() 方法到 handleMessage() 方法间的这段代码执行的极短暂时间消耗也充分考虑在内(这里其实主要考虑的是 Message 队列的排队时间)。
这也就刚好解释前面我们所遇到的问题。onTick 方法第一次回调时的参数并不是按照我们设定的倒计时时间设定的,也就出现 Log 日志中显示的非整秒倒计时。
知道原因后,解决方案自然也很简单。在 CountDownTimer 初始化时,将总的倒计时时长额外延长 0.5 秒即可,也就是 500 毫秒:
CountDownTimer timer = new CountDownTimer(5000 + 500, 1000){
// 省略相关代码
};复制代码
注意:可能有人要问了,为什么是 500 毫秒,而不是 501、600 毫秒呢?当然是可以的。从 start() 调用到 onTick() 回调,其实也就是一段代码的执行时间,是极短的。从前面的日志中也可以看到,那遍执行只消耗 15 毫秒(每次运行代码消耗时间均有所不同,取决于那个 Handler 所关联的 Message 队列实际使用情况)。所以,这个例子中只要是小于 1000 毫秒的合适增量值,理论上来讲都是可以的,只要不是太小。
还有一种解决方案,你可以对 millisUntilFinished 转换 float 类型求值,再利用 BigDecimal 提供的向上舍入模式转换为 int 类型。这种方式只是麻烦一些。
内存泄漏问题
前面提到 CountDownTimer 内部是利用 Handler 机制实现的,自然也就存在内存泄漏的问题。
当 Activity 关闭时,如果 CountDownTimer 没有倒计时完毕的话,就会在后台一直执行,并且持有 Activity 的引用,导致不必要的内存泄漏,甚至回调处理时发生空值异常错误。
所以,前文我们使用的方式不是很合理。应该将 CountDownTimer 定义成全局变量,然后在 Activity 销毁时取消倒计时:
@Override
protected void onDestroy() {
super.onDestroy();
if (timer != null) {
timer.cancel();
}
}复制代码
其中,cancel() 方法的内部源码如下:
public synchronized final void cancel() {
mCancelled = true;
mHandler.removeMessages(MSG);
}复制代码
主要是移除 Handler 相关联 Message 队列中的延时 Message 对象。
关于我:亦枫,博客地址:yifeng.studio/,新浪微博:IT亦枫
微信扫描二维码,欢迎关注我的个人公众号:安卓笔记侠
不仅分享我的原创技术文章,还有程序员的职场遐想