第一次写源码分析类博客,如有错误,欢迎讨论和指正~~ (^_^)
--------------------------------------
最近在写一个倒计时控件 CountdownView , 发现系统自带的 CountDownTimer onTick() 并不准确,当然,它的倒计时长度还是比较准确的。
本博客 demo 见: countdown
一、问题
CountDownTimer 使用比较简单,设置 5 秒的倒计时,间隔为 1 秒。
final String TAG = "CountDownTimer";
new CountDownTimer(5 * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
Log.i(TAG, "onTick → millisUntilFinished = " + millisUntilFinished + ", seconds = " + millisUntilFinished / 1000);
}
@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() 里显示也可以。
先看一下运行效果图:
(demo 的 log 前面的毫秒数是手机当前系统时间戳)
打印日志可以看到有几个问题:
问题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 <= 0) {
onFinish();
return this;
}
//Add
Log.i(TAG, "start → mMillisInFuture = " + mMillisInFuture + ", seconds = " + mMillisInFuture / 1000 );
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