Android 关于 CountDownTimer onTick() 倒计时不准确问题源码分析
一、问题
CountDownTimer 使用比较简单,设置 5 秒的倒计时,间隔为 1 秒。
final String TAG = "CountDownTimer"; new CountDownTimer( * , ) {
@Override
public void onTick(long millisUntilFinished) {
Log.i(TAG, "onTick → millisUntilFinished = " + millisUntilFinished + ", seconds = " + millisUntilFinished / );
} @Override
public void onFinish() {
Log.i(TAG, "onFinish");
}
}.start();
以 API 25 为例。即 app 的 build.gradle 中设置的编译版本是 25(后续会提到版本问题)。
compileSdkVersion 25
我们期待的效果是:“5-4-3-2-1-finish”或者“5-4-3-2-1-0”。这里,我认为 显示 0 和 finish 的时间应该是一致的,所以把 0 放在 onFinish() 里显示也可以。
打印日志可以看到有几个问题:
问题1. 每次 onTick() 都会有几毫秒的误差,并不是期待的准确的 "5000, 4000, 3000, 2000, 1000, 0"。
问题2. 多运行几次,就会发现这几毫秒的误差,导致了计算得出的剩余秒数并不准确,如果你的倒计时需要显示剩余秒数,就会发生 秒数跳跃/缺失 的情况(比如一开始从“4”开始显示——缺少“5”,或者直接从“5”跳到了“3”——缺少“4”)。
问题3. 最后一次 onTick() 到 onFinish() 的间隔通常超过了 1 秒,差不多是 2 秒左右。如果你的倒计时在显示秒数,就能很明显的感觉到最后 1 秒停顿的时间很长。
仔细看一下日志里标注的地方,如果你想直接看解决方案,可以直接滑到日志最下方,或者在顶部目录里选择最后一栏“三、终极解决”查看。
二、分析源码
(一)API 25 源码分析
查看 CountDownTimer 源码(API 25),
发现 start() 中计算的 mStopTimeInFuture(未来停止倒计时的时刻,即倒计时结束时间) 加了一个 SystemClock.elapsedRealtime() ,系统自开机以来(包括睡眠时间)的毫秒数,后文中以“系统时间戳”简称。
即倒计时结束时间为“当前系统时间戳 + 你设置的倒计时时长 mMillisInFuture ”,也就是计算出的相对于手机系统开机以来的一个时间。
继续往下看,多处用到了 SystemClock.elapsedRealtime() 。
在源码里添加 Log 打印看看。(直接在源码里修改是不会打印出来的,因为运行时不是编译的你刚刚修改的源码,而是手机里对应的源码。我复制了一份源码添加的 Log,见 demo 里的CountDownTimerCopyFromAPI25.java)
String TAG = "CountDownTimer-25";
/**
* Start the countdown.
*/
public synchronized final CountDownTimerCopyFromAPI25 start() {
mCancelled = false;
if (mMillisInFuture <= ) {
onFinish();
return this;
}
//Add
Log.i(TAG, "start → mMillisInFuture = " + mMillisInFuture + ", seconds = " + mMillisInFuture / );
mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
//Add
Log.i(TAG, "start → elapsedRealtime = " + SystemClock.elapsedRealtime());
Log.i(TAG, "start → mStopTimeInFuture = " + mStopTimeInFuture);
mHandler.sendMessage(mHandler.obtainMessage(MSG));
return this;
}
// handles counting down
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() { @Override
public void handleMessage(Message msg) { synchronized (CountDownTimerCopyFromAPI25.this) {
if (mCancelled) {
return;
} final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime(); //Add
Log.i(TAG, "handleMessage → elapsedRealtime = " + SystemClock.elapsedRealtime());
Log.i(TAG, "handleMessage → millisLeft = " + millisLeft + ", seconds = " + millisLeft / ); if (millisLeft <= ) {
//Add
Log.i(TAG, "onFinish → millisLeft = " + millisLeft);
onFinish();
} else if (millisLeft < mCountdownInterval) {
//Add
Log.i(TAG, "handleMessage → millisLeft < mCountdownInterval !");
// no tick, just delay until done
sendMessageDelayed(obtainMessage(MSG), millisLeft);
} else {
long lastTickStart = SystemClock.elapsedRealtime();
//Add
Log.i(TAG, "before onTick → lastTickStart = " + lastTickStart);
Log.i(TAG, "before onTick → millisLeft = " + millisLeft + ", seconds = " + millisLeft / );
onTick(millisLeft);
//Add
Log.i(TAG, "after onTick → elapsedRealtime = " + SystemClock.elapsedRealtime());
// take into account user's onTick taking time to execute
long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
//Add
Log.i(TAG, "after onTick → delay1 = " + delay);
// special case: user's onTick took more than interval to
// complete, skip to next interval
while (delay < ) delay += mCountdownInterval;
//Add
Log.i(TAG, "after onTick → delay2 = " + delay);
sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
};
打印日志:
倒计时 5 秒,而 onTick() 一共只执行了 4 次。
start() 启动计时时,mMillisInFuture = 5000。
且根据当前系统时间戳(记为 elapsedRealtime0 = 349001103,开始 start() 倒计时时的系统时间戳)计算了倒计时结束时相对于系统开机时的时间点 mStopTimeInFuture。
mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;//---------(1)
此后到第一次进入 handleMessage() 时,中间经历了很短的时间 349001109 - 349001103 = 6 毫秒。
handleMessage() 这里精确计算了程序执行时间,虽然是第一次进入 handleMessage,也没有直接使用 mStopTimeInFuture,而是根据程序执行到此处时的 elapsedRealtime() (记为 elapsedRealtime1)来计算此时剩余的倒计时时长。
final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();//---------(2)
根据 (1) 式和 (2) 式,调换一下运算顺序,其实就是
millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime()
= elapsedRealtime0 + mMillisInFuture - elapsedRealtime1
= mMillisInFuture - (elapsedRealtime1 - elapsedRealtime0)//减去程序从 start() 执行到此处花掉的时间
= - ( - )
=
millisLeft = 4994,进入 else,执行 onTick():
所以第一次 onTick() 时,millisLeft = 4994,导致计算的剩余秒数是“4994 / 1000 = 4”,所以倒计时显示秒数是从“4”开始,而不是“5”开始。这便是前面提到的 问题1 和 问题2。
onTick() 后还计算了下一次发送 message 的一个延迟时间 delay:
long lastTickStart = SystemClock.elapsedRealtime(); onTick(millisLeft); // take into account user's onTick taking time to execute
// 考虑到用户执行 onTick 需要时间
long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
lastTickStart = SystemClock.elapsedRealtime() 即此次触发 onTick() 前时的系统时间戳,
mCountdownInterval 即我们设置的 onTick() 的调用间隔。
两者相加,再减去执行完 onTick() 后时的系统时间戳,得到 delay 的值。
同样的,我们调换一下加减运算顺序,可以看到
delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime()
= mCountdownInterval - (SystemClock.elapsedRealtime() - lastTickStart)
= mCountdownInterval - 此次 onTick() 的执行时间 //看到这里其实就明白了,计算 delay 是为了保证 onTick() 每次调用时的间隔是 mCountdownInterval.
= - ( - )
=
可是日志里输出的 delay = 980,看看我们添加的打印 log 语句,
onTick(millisLeft);
//Add
Log.i(TAG, "after onTick → elapsedRealtime = " + SystemClock.elapsedRealtime());//----(3) // take into account user's onTick taking time to execute
// 考虑到用户执行 onTick 需要时间
long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();//-----(4)
可见在 (3) 式打印日志时到 (4) 式计算 delay 时中间刚好消耗了 1 毫秒。也就是计算 delay 时系统时间戳实际是 elapsedRealtime = 349001129 + 1 = 349001130。
所以我们的 mCountdownInterval 依然是每次 调用 onTick() 时的时间间隔。
继续往下看代码,发现在发送下一次 message 前,还对 delay 的值做了判断:
// 特殊情况:用户的 onTick 执行时间超过了给定的时间间隔 mCountdownInterval,则直接跳到下一次间隔
while (delay < ) delay += mCountdownInterval;
sendMessageDelayed(obtainMessage(MSG), delay);
如果这次 onTick() 执行时间太长,超过了 mCountdownInterval ,那么执行完 onTick() 后计算得到的 delay 是一个负数,此时直接跳到下一次 mCountdownInterval 间隔,让 delay + mCountdownInterval。
似乎有点绕,那我们带入具体的数值来计算一下吧。
我们设定每 1000 毫秒执行一次 onTick()。假设第一次 onTick() 开始前时的相对于手机系统开机时间的剩余倒计时时长是 5000 毫秒, 执行完这次 onTick() 操作消耗了 1005 毫秒,超出了我们设定的 1000 毫秒的间隔,那么第一次计算的 delay = 1000 - 1005 = -5 < 0,那么负数意味着什么呢?
本来我们设定的 onTick() 调用间隔是 1000 毫秒,可是它执行完一次却用了 1005 毫秒,现在剩余倒计时还剩下 5000 - 1005 = 3995 毫秒,本来第二次 onTick() 按期望应该是在 4000 毫秒时开始执行的,可是此时第一次的 onTick() 却还未执行完。所以第二次 onTick() 就会被延迟 delay = -5 + 1000 = 995 毫秒,也就是到剩余 3000 毫秒时再执行了。
回到我们的 log 里~第一次 onTick() 执行完后,log 打印出 elapsedRealtime = 349001129,前面分析了此时实际的系统时间戳其实是 349001129 + 1 = 349001130。然后延迟了 delay = 980 毫秒后,第二次进入 handleMessage(),我们计算此时系统时间戳为 349001130 + 980 = 349002110,和 log打印一致。再来计算此时的 millisLeft:
millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime()
= elapsedRealtime0 + mMillisInFuture - elapsedRealtime2
= mMillisInFuture - (elapsedRealtime2 - elapsedRealtime0)//减去程序从 elapsedRealtime0 执行到此处花掉的时间
= - ( - )
=
剩余秒数为 seconds = 3993 / 1000 = 3 秒。执行完第二次 onTick() 时的系统时间戳是 elapsedRealtime = 349002117,
delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime()
= mCountdownInterval - (SystemClock.elapsedRealtime() - lastTickStart)
= 1000 - (349002117 - 349002111)
= 994
后续第 3、4 次的计算就不写了,和上面的计算类似。
从日志可以看到,最后一次调用 onTick() 是在 第 4 次处理 handleMessage 时调用的,此时倒计时显示剩余 millisLeft = 1990 毫秒 = (int)(1990 /1000) 秒 = 1 秒。
此时 lastTickStart = 349004114,而 349004114 + 1990 =349006104,也就是 第 6 次 进入 handleMessage 时调用 onFinish() 的时间。
延迟了 delay = 996 毫秒后,接下来,第 5 次进入 handleMessage 时,因为 millisLeft = 988 < mCountdownInterval = 1000 ,导致没有触发 onTick(),而是直接发送了一个延迟了 millisLeft = 988 毫秒的 message。此时的 elapsedRealtime = 349005115。
延迟了 988 毫秒后,elapsedRealtime = 349005115 + 988 = 349006103,log 打印为 349006104,差不多。记 elapsedRealtime3= 349006104。
现在第 6 次进入 handleMessage,
millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime()
= elapsedRealtime0 + mMillisInFuture - elapsedRealtime3
= mMillisInFuture - (elapsedRealtime3 - elapsedRealtime0)//减去程序从 start() 执行到此处花掉的时间
= - ( - )
= -
millisLeft = -1 < 0,调用 finish(),结束倒计时~
所以在 第 4 次 handleMessage() 后就没有再触发 onTick() 了,而且从前面分析处标红文字可以看到,最后一次 onTick() 调用后,一共延迟了 2 次,共 996 + 988 = 1984 ≈ 1990 毫秒,才执行到 onFinish()。这便是文章初提到的问题3:倒计时最后 1 秒停顿时间过长。
至此,关于 API 25 里的 CountDownTimer 源码分析完毕,所以其实源码也并不是绝对正确的,我们发现了有几处问题。接下来针对这几处问题来分析一下如何改进~
(二)API 25 源码改进
针对 问题1 和 问题 2:
问题1. 每次 onTick() 都会有几毫秒的误差,并不是期待的准确的 "5000, 4000, 3000, 2000, 1000, 0"。
问题2. 多运行几次,就会发现这几毫秒的误差,导致了计算得出的剩余秒数并不准确,如果你的倒计时需要显示剩余秒数,就会发生 秒数跳跃/缺失 的情况(比如一开始从“4”开始显示——缺少“5”,或者直接从“5”跳到了“3”——缺少“4”)。
这 2 个问题可以放在一起处理,网上也有很多人对这里做了改进,那就是给我们的 倒计时时长扩大一点点,通常是 手动将 mMillisInFuture 扩大几十毫秒,比如文章开头的例子,可以在 new CountDownTimer() 时修改传参:
final String TAG = "CountDownTimer";
new CountDownTimer( * + , ) { // 方案1:修改构造方法的传参
@Override
public void onTick(long millisUntilFinished) {
Log.i(TAG, "onTick → millisUntilFinished = " + millisUntilFinished + ", seconds = " + millisUntilFinished / );
} @Override
public void onFinish() {
Log.i(TAG, "onFinish");
}
}.start();
这里多加了 20 毫秒,运行一下(具体代码可见 demo,这里只是举个栗子)
倒计时:“5,4,3,2,1,finish”,
基本可以解决 问题1 和 问题2 啦~
当然,你也可以写一个自己的 CountdownTimer,在构造方法里修改,这样就不用每次调用时手动改时长了:
public MyCountDownTimer(long millisInFuture, long countDownInterval) {
mMillisInFuture = millisInFuture + ; // 方案2:直接在构造方法里修改 mMillisInFuture
mCountdownInterval = countDownInterval;
}
针对 问题3:
问题3. 最后一次 onTick() 到 onFinish() 的间隔通常超过了 1 秒,差不多是 2 秒左右。如果你的倒计时在显示秒数,就能很明显的感觉到最后 1 秒停顿的时间很长。
其实我们增加了 20 毫秒后,查看日志就发现这个延迟也变小了,几乎和 最后一次 onTick() 一致了,所以如果你需要最后显示 0 ,而又不需要在 onFinish() 里做什么的话,修改至此就 ok 啦~
我们看看之前有问题的日志呢,可以发现 第 5 次进入 handleMessage() 时,因为 millisLeft = 988 < 1000,所以会进入 else if 的逻辑:
这里按期望应该是要执行一次 onTick() 。
所以我们加上一句 onTick() 即可。
打印日志:
修改后的完整代码见:CountDownTimerImproveFromAPI25.java
不过这也有个问题,因为我们是直接将倒计时时间加长了,虽然只是几十毫秒,但也会造成整个倒计时的时间(从 start() 到 onFinish())不是精确的,而且这个 20 毫秒只是我根据前面程序运行的时间规律算的,可能也有程序从 start() 运行到 第一次进入 handleMessage() 会超过 20 毫秒的情况呢?
(三)API 26 源码分析
问题1. 每次 onTick() 都会有几毫秒的误差,并不是期待的准确的 "5000, 4000, 3000, 2000, 1000, 0"。
问题2. 这几毫秒的误差,导致了计算得出的剩余秒数并不准确,如果你的倒计时需要显示剩余秒数,就会发生 秒数跳跃/缺失 的情况(比如一开始从“4”开始显示——缺少“5”,或者直接从“5”跳到了“3”——缺少“4”),并且都没有显示 “0”秒。
问题3. 最后一次 onTick() 显示为 0 ,到 onFinish() 的间隔约有 1 秒。
其中问题1 和 问题2 和 API 25 的一致,不再详述。
看一下 API 26 的代码吧,demo 中见 CountDownTimerCopyFromAPI26.java
private Handler mHandler = new Handler() { @Override
public void handleMessage(Message msg) { synchronized (CountDownTimerCopyFromAPI26.this) {
if (mCancelled) {
return;
} final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime(); //Add
Log.i(TAG, "handleMessage → elapsedRealtime = " + SystemClock.elapsedRealtime());
Log.i(TAG, "handleMessage → millisLeft = " + millisLeft + ", seconds = " + millisLeft / ); if (millisLeft <= ) {
//Add
Log.i(TAG, "onFinish → millisLeft = " + millisLeft); onFinish();
} else {
long lastTickStart = SystemClock.elapsedRealtime(); //Add
Log.i(TAG, "before onTick → lastTickStart = " + lastTickStart);
Log.i(TAG, "before onTick → millisLeft = " + millisLeft + ", seconds = " + millisLeft / ); onTick(millisLeft); // take into account user's onTick taking time to execute
// 考虑到用户执行 onTick 需要时间
long lastTickDuration = SystemClock.elapsedRealtime() - lastTickStart;
long delay; //Add
Log.i(TAG, "after onTick → lastTickDuration = " + lastTickDuration); if (millisLeft < mCountdownInterval) {
// just delay until done
//直接延迟到计时结束
delay = millisLeft - lastTickDuration; //Add
Log.i(TAG, "after onTick → delay1 = " + delay); // special case: user's onTick took more than interval to
// complete, trigger onFinish without delay
// 特殊情况:用户的 onTick 执行时间超过了给定的时间间隔 mCountdownInterval,则立即触发 onFinish
if (delay < ) delay = ; //Add
Log.i(TAG, "after onTick → delay2 = " + delay);
} else {
delay = mCountdownInterval - lastTickDuration; //Add
Log.i(TAG, "after onTick → delay1 = " + delay); // special case: user's onTick took more than interval to
// complete, skip to next interval
// 特殊情况:用户的 onTick 执行时间超过了给定的时间间隔 mCountdownInterval,则直接跳到下一次间隔
while (delay < ) delay += mCountdownInterval; //Add
Log.i(TAG, "after onTick → delay2 = " + delay);
} sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
};
三、终极解决
final String TAG = "CountDownTimer"; new CountDownTimer( * , ) {
@Override
public void onTick(long millisUntilFinished) {
//四舍五入取整
Log.i(TAG, "onTick → millisUntilFinished = " + millisUntilFinished + ", seconds = " + Math.round((double) millisUntilFinished / ));
} @Override
public void onFinish() {
Log.i(TAG, "onFinish");
}
}.start();
seconds = Math.round((double) millisecond / 1000);
Android 关于 CountDownTimer onTick() 倒计时不准确问题源码分析的更多相关文章
- Android事件传递机制详解及最新源码分析——ViewGroup篇
版权声明:本文出自汪磊的博客,转载请务必注明出处. 在上一篇<Android事件传递机制详解及最新源码分析--View篇>中,详细讲解了View事件的传递机制,没掌握或者掌握不扎实的小伙伴 ...
- Android多线程之(一)View.post()源码分析——在子线程中更新UI
提起View.post(),相信不少童鞋一点都不陌生,它用得最多的有两个功能,使用简便而且实用: 1)在子线程中更新UI.从子线程中切换到主线程更新UI,不需要额外new一个Handler实例来实现. ...
- Android事件传递机制详解及最新源码分析——View篇
摘要: 版权声明:本文出自汪磊的博客,转载请务必注明出处. 对于安卓事件传递机制相信绝大部分开发者都听说过或者了解过,也是面试中最常问的问题之一.但是真正能从源码角度理解具体事件传递流程的相信并不多, ...
- Android事件传递机制详解及最新源码分析——Activity篇
版权声明:本文出自汪磊的博客,转载请务必注明出处. 在前两篇我们共同探讨了事件传递机制<View篇>与<ViewGroup篇>,我们知道View触摸事件是ViewGroup传递 ...
- Android应用AsyncTask处理机制详解及源码分析
1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ...
- 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...
- 【转载】Android应用AsyncTask处理机制详解及源码分析
[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个 ...
- Android 网络流量监听开源项目-ConnectionClass源码分析
很多App要做到极致的话,对网络状态的监听是很有必要的,比如在网络差的时候加载质量一般的小图,缩略图,在网络好的时候,加载高清大图,脸书的android 客户端就是这么做的, 当然伟大的脸书也把这部分 ...
- Android应用层View绘制流程与源码分析
1 背景 还记得前面<Android应用setContentView与LayoutInflater加载解析机制源码分析>这篇文章吗?我们有分析到Activity中界面加载显示的基本流程原 ...
随机推荐
- K2 BPM_如何将RPA的价值最大化?_全球领先的工作流引擎
自动化技术让企业能够更有效的利用资源,减少由于人为失误而造成的风险损失.随着科技的进步,实现自动化的途径变得更加多样化. 据Forrester预测,自动化技术将在2019年成为引领数字化转型的前沿技 ...
- es中的相关知识一(基本知识和id的定义)
一.es中文档的元数据包括: 1._index: 索引(index)类似于关系型数据库里的数据库(database),事实上,我们的数据被存储和索引在分片(shards)中,索引知识把一个或多个分片分 ...
- 关于从入 OI 以来学的各种知识点的系统总结
前言 OI 之路差不多快结束了,最近水平也萎得很厉害,这里就开个目录,记录一些需要总结的知识点吧.不定期更,勿催,我还要改模拟赛的题. 目录
- 域知识深入学习二:建立AD DS域
2.1 建立AD DS域前的准备工作 先安装一台服务器,然后将其升级(promote)为域控 2.1.1 选择适当的DNS域名 AD DS域名采用DNS的架构与命名方式 2.1.2 准备好一台支持AD ...
- grep匹配命令
关于匹配的实例: 统计所有包含“48”字符的行有多少行 grep -c "48" demo.txt 不区分大小写查找“May”所有的行) grep -i "May&q ...
- iotop命令详解
iotop是top和iostat程序的混合体,能够显示系统中所有运行进程并将进程根据I/O统计信息排序. 这个软件使用了Linux内核的一些新特性,所以需要2.6.20或者更新的内核. 一般默认情况下 ...
- vmware添加新硬盘磁盘扫描脚本
#! /bin/bash echo "- - -" > /sys/class/scsi_host/host0/scan echo "- - -" > ...
- C# 动态语言扩展(11)
在 C# 4 开始添加 dynamic 类型.Mono C# 已经支持 C# 6.0 了. DLR C# 4 动态功能是 Dynamic Language Runtime (动态语言运行时,DLR)的 ...
- 删除3天前创建的以log结尾的文件
1. #/bin/bash # filename: del_log.sh find / -name "*.log" -mtime 3 | xargs rm -rf 2. #/bin ...
- OFDM留空中央直流子载波目的及原理
目的: 降低峰均比! 原理: IDFT公式: 直流分量k接近0,公式近似于对X(k)进行累加,因此直流分量会产生较大的信号能量,造成严重的峰均比. 详细内容可参考: https://dwz.cn/Zl ...