第一次寫源碼分析類博客,如有錯誤,歡迎討論和指正~~ (^_^)
--------------------------------------
最近在寫一個倒計時控件 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 + ", seconds = " + millisLeft / 1000 );
if (millisLeft <= 0) {
//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 / 1000 );
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 < 0) 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() 執行到此處花掉的時間
= 5000 - (349001109 - 349001103)
= 4994
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.
= 1000 - (349001129 - 349001110)
= 981
可是日誌裏輸出的 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 的值做了判斷:
// special case: user's onTick took more than interval to
// complete, skip to next interval
// 特殊情況:用戶的 onTick 執行時間超過了給定的時間間隔 mCountdownInterval,則直接跳到下一次間隔
while (delay < 0) 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 執行到此處花掉的時間
= 5000 - (349002110 - 349001103)
= 3993
剩餘秒數爲 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() 執行到此處花掉的時間
= 5000 - (349006104 - 349001103)
= -1
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(5 * 1000 + 20, 1000) { // 方案1:修改構造方法的傳參
@Override
public void onTick(long millisUntilFinished) {
Log.i(TAG, "onTick → millisUntilFinished = " + millisUntilFinished + ", seconds = " + millisUntilFinished / 1000);
}
@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 + 20; // 方案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 / 1000);
if (millisLeft <= 0) {
//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 / 1000);
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 < 0) delay = 0;
//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 < 0) delay += mCountdownInterval;
//Add
Log.i(TAG, "after onTick → delay2 = " + delay);
}
sendMessageDelayed(obtainMessage(MSG), delay);
}
}
}
};
三、終極解決
final String TAG = "CountDownTimer";
new CountDownTimer(5 * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
//四捨五入取整
Log.i(TAG, "onTick → millisUntilFinished = " + millisUntilFinished + ", seconds = " + Math.round((double) millisUntilFinished / 1000));
}
@Override
public void onFinish() {
Log.i(TAG, "onFinish");
}
}.start();
seconds = Math.round((double) millisecond / 1000);