如何为CountDownTimer的resume操作添加非阻塞2秒延迟
问题原因
之前直接用sendMessageDelayed添加2秒延迟出现主线程卡死、逻辑异常,核心是两个逻辑漏洞:
- 调用
resume时立刻计算了mStopTimeInFuture(基于当前系统启动时间加剩余倒计时时长),但实际倒计时2秒后才启动,等Handler收到消息执行时,系统时间已经多走了2秒,会导致timeLeft()计算出的剩余时间直接为负,打乱原有倒计时的消息调度逻辑。 - 延迟等待的2秒内直接把
mPauseTimeRemaining设为0,导致计时器状态判定错误(此时既不属于暂停也不属于运行),cancel方法也没有清理延迟启动的消息,容易出现消息重复发送、锁等待异常。
修复方案
不需要改动原有倒计时核心调度逻辑,只需要单独处理resume前的2秒延迟状态,复用已经定义但未使用的MSG2作为延迟启动的消息标记即可,全程基于主线程Handler调度,不会阻塞主线程,也不会破坏原有倒计时精度:
- 修改
cancel方法,同时移除倒计时消息和延迟启动消息 - 修改
resume逻辑,延迟2秒发送启动消息,等待期间保持暂停状态,不提前计算停止时间 - 在Handler的消息处理中新增
MSG2的分支,收到启动消息后再执行真正的倒计时启动逻辑
改动后的核心代码
/** * Cancel the countdown and clears all remaining messages */ fun cancel() { // 同时移除两类消息,避免残留消息触发异常 mHandler.removeMessages(MSG) mHandler.removeMessages(MSG2) } /** * Resumes the counter. */ fun resume() { if (isPaused) { // 先清空所有残留消息,避免重复触发 cancel() // 延迟2秒发送启动消息,不阻塞主线程 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG2), 2000) } } // handles counting down private val mHandler: Handler = object : Handler(Looper.getMainLooper()) { override fun handleMessage(msg: Message) { synchronized(this@CountDownTimer) { when(msg.what) { MSG2 -> { // 2秒延迟到了,真正启动倒计时 mMillisInFuture = mPauseTimeRemaining // 此时再计算停止时间,保证时间精度 mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture sendMessage(obtainMessage(MSG)) mPauseTimeRemaining = 0 } MSG -> { // 原有倒计时逻辑完全不动,保证原有运行逻辑不受影响 val millisLeft = timeLeft() if (millisLeft <= 0) { cancel() onFinish() } else if (millisLeft < mCountdownInterval) { // no tick, just delay until done sendMessageDelayed(obtainMessage(MSG), millisLeft) } else { val lastTickStart = SystemClock.elapsedRealtime() onTick(millisLeft) // take into account user's onTick taking time to execute var delay = mCountdownInterval - (SystemClock.elapsedRealtime() - lastTickStart) // special case: user's onTick took more than mCountdownInterval to // complete, skip to next interval while (delay < 0) delay += mCountdownInterval sendMessageDelayed(obtainMessage(MSG), delay) } } } } } }
补充说明
- 2秒等待期间调用
pause、cancel都会正常移除延迟启动消息,不会出现倒计时偷跑的问题 - 2秒等待时间不会计入倒计时时长,恢复后剩余时间和暂停时完全一致
- 原有
onTick、onFinish的触发逻辑、时间精度完全没有改动,不会破坏原有业务逻辑 - 如果不需要首次创建启动时的2秒延迟,可以给
resume加个可选参数skipDelay: Boolean = false,首次创建调用resume(true)跳过延迟即可
内容的提问来源于stack exchange,提问作者BroscR
相关产品推荐
相关产品推荐

