Android 實現倒計時的方式有多種,Handler 延時發送 Message,Timer 和 TimerTask 配合使用,使用 CountDownTimer 類等。相比而言,通過系統封裝的 CountDownTimer 算是使用起來最爲方便的方式之一。java
然而,CountDownTimer 有兩個使用上的問題咱們不得不稍加註意:計時不許確、內存泄漏問題。咱們來結合源碼逐一分析一下。程序員
舉個簡單的例子,利用 CountDownTimer 實現一個時長爲 5 秒的 View 倒計時顯示,要求從 5s 開始每隔 1 秒倒計時顯示到 1s:bash
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 回調方法中的參數有問題,那就在該方法中添加一句日誌:ide
Log.d("onTick", millisUntilFinished + "");複製代碼
打印結果以下:this
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 毫秒開始倒計時的!spa
因而分析 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;
}複製代碼
可以看出,CountDownTimer 在內部也是藉助 Handler 實現的。同時,初始化 CountDownTimer 時的 millisInFuture 參數將轉化成 mStopTimeInFuture 值。值得注意的是,在轉化的同時,還自動添加這個時間:code
SystemClock.elapsedRealtime()複製代碼
這個表達式的時間值表示系統啓動到當前程序代碼執行時的時間毫秒數。先暫且無論,繼續看這個 Handler 的實現:cdn
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){
// 省略相關代碼
};複製代碼
注意:可能有人要問了,爲何是 500 毫秒,而不是 50一、600 毫秒呢?固然是能夠的。從 start() 調用到 onTick() 回調,其實也就是一段代碼的執行時間,是極短的。從前面的日誌中也能夠看到,那遍執行只消耗 15 毫秒(每次運行代碼消耗時間均有所不一樣,取決於那個 Handler 所關聯的 Message 隊列實際使用狀況)。因此,這個例子中只要是小於 1000 毫秒的合適增量值,理論上來說都是能夠的,只要不是過小。
還有一種解決方案,你能夠對 millisUntilFinished 轉換 float 類型求值,再利用 BigDecimal 提供的向上舍入模式轉換爲 int 類型。這種方式只是麻煩一些。
前面提到 CountDownTimer 內部是利用 Handler 機制實現的,天然也就存在內存泄漏的問題。
當 Activity 關閉時,若是 CountDownTimer 沒有倒計時完畢的話,就會在後臺一直執行,而且持有 Activity 的引用,致使沒必要要的內存泄漏,甚至回調處理時發生空值異常錯誤。
因此,前文咱們使用的方式不是很合理。應該將 CountDownTimer 定義成全局變量,而後在 Activity 銷燬時取消倒計時:
@Override
protected void onDestroy() {
super.onDestroy();
if (timer != null) {
timer.cancel();
}
}複製代碼
其中,cancel() 方法的內部源碼以下:
public synchronized final void cancel() {
mCancelled = true;
mHandler.removeMessages(MSG);
}複製代碼
主要是移除 Handler 相關聯 Message 隊列中的延時 Message 對象。
關於我:亦楓,博客地址:yifeng.studio/,新浪微博:IT亦楓
微信掃描二維碼,歡迎關注個人我的公衆號:安卓筆記俠
不只分享個人原創技術文章,還有程序員的職場遐想