前言
在Android應用中幾乎都能看到,閃屏頁或者歡迎頁右上角有一個 “倒計時 + 跳過” 的功能,還有就是獲得驗證碼的倒計時功能,Android 實現倒計時的方式有多種,Handler 延時發送 Message,Timer 和 TimerTask 配合使用,使用 CountDownTimer 類等。相比而言,經過系統封裝的 CountDownTimer 算是使用起來最爲方便的方式之一。
CountDownTimer的使用示例
// 參數1:倒計時的總時間,單位爲毫秒
// 參數2:每次遞減的時間,單位也爲毫秒
private CountDownTimer timer = new CountDownTimer(10000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
// 每隔參數二指定的毫秒數就會回調一次該方法,直到剩餘毫秒數millisUntilFinished減去參數2設置的每次遞減毫秒數
的結果小於參數2設置的每次遞減毫秒數時,onTick方法就不會被回調了,會直接回調onFinish方法。
vertifyView.setText((millisUntilFinished / 1000) + "秒後可重發");
}
@Override
public void onFinish() {
// 倒計時完成之後,會回調該方法
vertifyView.setEnabled(true);
vertifyView.setText("獲取驗證碼");
}
};
// 開始執行倒計時
timer.start();
CountDownTimer使用過程中的2個問題
1.開始時間不準確以及不能倒計時到0
2.使用過程中有可能會出現內存泄漏
2個問題產生的原因分析以及解決方案
開始時間不準確以及不能倒計時到0
執行下面的代碼:
CountDownTimer timer = new CountDownTimer(5000, 1000){
@Override
public void onTick(long millisUntilFinished) {
Log.d("onTick", millisUntilFinished + "");
mSampleTv.setText((int)millisUntilFinished / 1000 + "s");
}
@Override
public void onFinish() {
}
};
timer.start();
我們發現TextView並不是以5,4,3,2,1,0這樣來顯示的,而是以4,3,2,1來顯示,開始竟然是4不是5,同樣結束也不是0而是1。
控制檯打印的日誌信息如下:
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
經過對源碼的分析,我們發現CountDownTimer 在內部也是藉助 Handler 實現的,並且CountDownTimer 的內部實現比我們理想的計算更加精準,將 start() 方法到 handleMessage() 方法間的這段代碼執行的極短暫時間消耗也充分考慮在內(這裏其實主要考慮的是 Message 隊列的排隊時間),而這也就是爲什麼log日誌中顯示的非整秒倒計時的原因。
對於倒計時不能到0的原因其實就是因爲1981-1000=981<1000,從而在1981之後沒有再次回調onTick方法,而是直接回調了onFinish方法。
知道了原因後,我們就可以通過延長時間的方式來補上這個誤差,至於補多少,一般建議小於1000的任意值即可,但是儘量不要取太小,比如我們可以取600,那麼代碼就變成如下:
CountDownTimer timer = new CountDownTimer(5000 + 600, 1000){
@Override
public void onTick(long m) {
Log.d("onTick", m + "");
mSampleTv.setText((int)m / 1000 + "s");
}
@Override
public void onFinish() {
}
};
timer.start();
對於倒計時不能到0的問題,有的朋友可能會說是否可以在onFinis()方法中直接設置TextView的值爲0秒呢,告訴你,我試過不起作用,不相信大家可以自己去試,那麼有沒有其他方式來解決呢,當然有,同樣還是在總時間上動手腳,我們可以在總時間上加1000毫秒,然後在顯示的時候再減去這1秒,代碼如下:
CountDownTimer timer = new CountDownTimer(5000 + 1600, 1000){
@Override
public void onTick(long m) {
Log.d("onTick", millisUntilFinished + "");
mSampleTv.setText(((int)m / 1000 -1)+ "s");
}
@Override
public void onFinish() {
}
};
timer.start();
可以看到這次顯示的結果即爲:5s,4s,3s,2s,1s,0s。
使用過程中有可能會出現內存泄漏
從源碼中我們可以看出,CountDownTimer的內部實現是採用Handler機制,通過sendMessageDelayed延遲發送一條message到主線程的looper中,然後在自身中收到之後判斷剩餘時間,併發出相關回調,然後再次發出message的方式。
這樣的方式其實是有一定弊端的,那就是如果在Activity或者Fragment被回收時並未調用CountDownTimer的cancel()方法結束自己,這個時候CountDownTimer的Handler方法中如果判斷到當前的時間未走完,那麼會繼續調用sendMessageDelayed(obtainMessage(MSG), delay);
,從而觸發onTick方法的執行,當回調了Activity或者fragment中CountDownTimer的onTick方法時,Activity或者Fragment已經被系統回收,從而裏面的變量被設置爲Null,此時如果再次調用mSampleTv.setText((int)m / 1000 + "s");
,mSampleTv爲空,也就會報空指針,同時,CountDownTimer中的Handler方法還在繼續執行,這一塊空間始終無法被系統回收也就造成了內存泄漏問題。
因此爲了避免空指針和內存泄露,我們要注意以下幾點:
1,在CountDownTimer的onTick方法中記得判空
activity中
if(!activity.isFinishing()){
//doing something...
}
fragment中
if(getActivity()!=null){
//doing something...
}
2,在配合DialogFragment使用時,如果在onFinish()方法調用了 dismiss()方法讓彈框消失,記得 判斷getFragmentManager是否爲空
@Override
public void onFinish() {
if(getFragmentManager()!=null){
dismiss();
}
}
3,在使用CountDownTimer時,在宿主Activity或fragment生命週期結束的時候,記得調用timer.cancle()方法
@Override
public void onDestroy() {
if(timer!=null){
timer.cancel();
timer = null;
}
super.onDestroy(); }
使用示例
public class ZpTimerActivity extends Activity {
private CountDownTimer mTimer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_timer);
initView();
}
private void initView() {
if (mTimer == null) {
mTimer = new CountDownTimer((long) (5 * 1000), 1000) {
@Override
public void onTick(long millisUntilFinished) {
if (!ZpTimerActivity.this.isFinishing()) {
int remainTime = (int) (millisUntilFinished / 1000L);
Log.e("zpan","======remainTime=====" + remainTime);
}
}
@Override
public void onFinish() {
Log.e("zpan","======onFinish=====");
}
};
}
mTimer.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
}