CountDownTimer 使用过程中的注意事项 时间跳跃不准 内存溢出

探讨Android中CountDownTimer的计时不准确与内存泄漏问题,分析其内部实现原理,提供解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;

}

 

SystemClock.elapsedRealtime()

 

能够看出,CountDownTimer 在内部也是借助 Handler 实现的。同时,初始化 CountDownTimer 时的 millisInFuture 参数将转化成 mStopTimeInFuture 值。值得注意的是,在转化的同时,还自动添加这个时间:

这个表达式的时间值表示系统启动到当前程序代码执行时的时间毫秒数。先暂且不管,继续看这个 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){

// 省略相关代码

};


还有一种解决方案,你可以对 millisUntilFinished 转换 float 类型求值,再利用 BigDecimal 提供的向上舍入模式转换为 int 类型。这种方式只是麻烦一些。注意:可能有人要问了,为什么是 500 毫秒,而不是 501、600 毫秒呢?当然是可以的。从 start() 调用到 onTick() 回调,其实也就是一段代码的执行时间,是极短的。从前面的日志中也可以看到,那遍执行只消耗 15 毫秒(每次运行代码消耗时间均有所不同,取决于那个 Handler 所关联的 Message 队列实际使用情况)。所以,这个例子中只要是小于 1000 毫秒的合适增量值,理论上来讲都是可以的,只要不是太小。

内存泄漏问题


前面提到 CountDownTimer 内部是利用 Handler 机制实现的,自然也就存在内存泄漏的问题。

当 Activity 关闭时,如果 CountDownTimer 没有倒计时完毕的话,就会在后台一直执行,并且持有 Activity 的引用,导致不必要的内存泄漏,甚至回调处理时发生空值异常错误。

所以,前文我们使用的方式不是很合理。应该将 CountDownTimer 定义成全局变量,然后在 Activity 销毁时取消倒计时:

@Override

protected void onDestroy() {

super.onDestroy();

if (timer != null) {

timer.cancel();

}

}

 

public synchronized final void cancel() {

mCancelled = true;

mHandler.removeMessages(MSG);

}

 

其中,cancel() 方法的内部源码如下:

主要是移除 Handler 相关联 Message 队列中的延时 Message 对象。

文章转哉的 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值