安卓项目实战之:倒计时类CountDownTimer的使用详解

前言

在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;
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章