【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析

本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
视频地址:http://www.cniao5.com/course/10121


倒计时功能常见应用场景:手机发送验证码

以前写定时器的常用做法:Handler + Timer + TimeTask,这个方法可以抛弃啦,android 为我们封装好了一个 CountDownTimer 类。

先来看看一个 Demo,倒计时器的应用。


【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析_第1张图片

一、倒计时器的使用

if (null != mLoginView){
   mLoginView.verifyCodeSuccess(60,60);
}

@Override
public void verifyCodeSuccess(int reaskDuration, int expireDuration) {
    showMsg("注册短信下发,验证码 " + expireDuration / 60 + " 分钟内有效!");
    OtherUtils.startTimer(new WeakReference(tvVerifyCode), "验证码", reaskDuration, 1);
}

/**
  * 在按钮上启动一个定时器
  * @param tvVerifyCode  验证码控件
  * @param defaultString 按钮上默认的字符串
  * @param max           失效时间(单位:s)
  * @param interval      更新间隔(单位:s)
  */
public static void startTimer(final WeakReference tvVerifyCode,
                                  final String defaultString,
                                  int max,
                                  int interval) {
    tvVerifyCode.get().setEnabled(false);
    // 由于CountDownTimer并不是准确计时,在onTick方法调用的时候,time会有1-10ms左右的误差,这会导致最后一秒不会调用onTick()
    // 因此,设置间隔的时候,默认减去了10ms,从而减去误差。
    // 经过以上的微调,最后一秒的显示时间会由于10ms延迟的积累,导致显示时间比1s长max*10ms的时间,其他时间的显示正常,总时间正常
    new CountDownTimer(max * 1000, interval * 1000 - 10) {
        @Override
        public void onTick(long time) {
            // 第一次调用会有1-10ms的误差,因此需要+15ms,防止第一个数不显示,第二个数显示2s
            if (null == tvVerifyCode.get())
                this.cancel();
            else
                tvVerifyCode.get().setText("" + ((time + 15) / 1000) + "s");
        }
        @Override
        public void onFinish() {
            if (null == tvVerifyCode.get()) {
                this.cancel();
                return;
            }
            tvVerifyCode.get().setEnabled(true);
            tvVerifyCode.get().setText(defaultString);
        }
    }.start();
}

从代码中提炼出来的就这么几步:

1、构造函数 CountDownTimer (long millisInFuture, long countDownInterval)
millisInFuture: 倒计时的总时间, 单位ms.
countDownInterval: 倒计时的间隔时间, 单位ms.

2、重写 onTick(long time) 和 onFinish() 方法

CountDownTimer 的使用很简单,只会使用的程序员不是好程序猿,当然要知道实现原理啦。

二、CountDownTimer 官方示例

【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析_第2张图片

看到官方示例以及英文解释,什么意思?

先看代码,代码中创建了一个 CountDownTimer 匿名内部类,实现了两个方法 onTick(long millisUntilFinished) 和 onFinish()。onTick() 方法每隔 1 秒回调一次,显示时间,onFinish() 方法在倒计时结束时候调用。

上面的英文释义:直到今后一段时间,定义一个倒计时,定期间隔通知,在本文字段中展示一个 30 秒倒计时示例。

下面的英文释义:对 onTick(long) 的调用与本对象同步,为了唯一调用 onTick(long),以便在上一次调用完成之前不会发生对 onTick(long) 的再一次调用。这有效仅在当执行 onTick(long) 时,执行时间和倒计时时间间隔相比的时候有意义。

翻译不到位,习惯看英文的还是看英文吧。

简言之,onTick(long) 这个方法是同步的,避免在上一次没调用完的时候重复调用,而有效时间就是倒计时时间间隔啦。

三、CountDownTimer 源码解析

3.1、基本结构

CountDownTimer 成员变量和成员函数均比较少,图中已经给出很详细的解释了,看到方法名你就知道有什么作用了。


【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析_第3张图片

通过源码可知,CountDownTimer 采用的是 Handler 机制,通过 sendMessageDelayed 延迟发送一条 message 到主线程的 looper 中,然后在自身中收到之后判断剩余时间,并发出相关回调,然后再次发出 message 的方式。

public abstract class CountDownTimer {
    //剩余的总时间, 单位ms.
    private final long mMillisInFuture;
    //间隔时间, 单位ms.
    private final long mCountdownInterval;
    private long mStopTimeInFuture;
    //是否取消
    private boolean mCancelled = false;
    
    public CountDownTimer(long millisInFuture, long countDownInterval) {
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;
    }
    //取消当前的任务
    public synchronized final void cancel() {
        mCancelled = true;
        mHandler.removeMessages(MSG);
    }
    //开始当前的任务
    public synchronized final CountDownTimer start() {
        mCancelled = false;
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
        //SystemClock.elapsedRealtime():返回系统启动到现在的毫秒数,包含休眠时间。
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }
    //当前任务每完成一次倒计时间隔时间时回调
    public abstract void onTick(long millisUntilFinished);
    //当前任务完成的时候回调
    public abstract void onFinish();
    //MSG
    private static final int MSG = 1;
    //主要是处理对 millisLeft 剩余时间的判断,其中 delay 的处理需要注意,当 onTick() 方法耗时过长会进行跳过
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            synchronized (CountDownTimer.this) {
                if (mCancelled) {
                    return;
                }
                //剩余时间,这就是上例中为什么有大约 1-10ms 的误差的原因了
                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()
                    onTick(millisLeft);
                    // take into account user's onTick taking time to execute
                    //处理用户onTick执行的时间,时间间隔始终和么算出来的
                    //启动时间 + 指定时间间隔 - 现在的时间
                    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                    // special case: user's onTick took more than interval to
                    // complete, skip to next interval
                    //特殊情况:用户的onTick方法花费的时间比interval长,那么直接跳转到下一次interval
                    while (delay < 0) delay += mCountdownInterval;
                    //发送延迟时间
                    sendMessageDelayed(obtainMessage(MSG), delay);
                }
            }
        }
    };
}

3.2、CountDownTimer 执行流程图

根据 CountDownTimer 的执行过程,以及 Handler 的源码,可以用流程图来描述倒计时的执行流程:


【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析_第4张图片

synchronized 关键字

public synchronized final void cancel();
public synchronized final CountDownTimer start();
private Handler mHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
      synchronized (CountDownTimer.this) {}
  };
}

在源码中,cancle(),start() 函数被 synchronized 修饰,handleMessage(Message msg) 中代码段也被 synchronized 修饰,它主要是用来保证在同一时刻,至多只有一个线程执行该段代码,主要有以下两个特点:
1、修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法, 作用的对象是调用这个方法的对象;
2、修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码, 作用的对象是调用这个代码块的对象;当某个线程访问的某具体对象的代码块被 synchronized 修饰时,其它线程对象中所有被 synchronized 修饰的代码块都会被阻塞,没有被 synchronized 的代码块访问正常。

关于 synchronized 的更多细节,请查看 Java中Synchronized的用法

在上面的示例中用到了弱引用,笔者一直没有提起,查了资料,学习之后发现,远远不止想象中的那些知识点,只能在写一篇文章中进行讲解啦。

【从 0 开始开发一款直播 APP】8 弱引用 WeakReference

参考:
http://blog.qiji.tech/archives/7485
http://blog.csdn.net/qq_20785431/article/details/51571300

【五一大促】菜鸟窝全场android项目实战课程低至五折,更有价值33元的四款热门技术免费领,17年初优惠力度最大的一次活动,有意向的童鞋不要错过
狂戳>>http://www.cniao5.com/hd/2017/51.html

你可能感兴趣的:(【从 0 开始开发一款直播 APP】7 倒计时器 CountDownTimer 源码解析)