前言
在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;
}
}
}