倒计时方案深入分析

目录介绍

  • 01.使用多种方式实现倒计时
  • 02.各种倒计时器分析
  • 03.CountDownTimer解读
  • 04.Timer和TimerTask解读
  • 05.自定义倒计时器案例

01.使用多种方式实现倒计时

  • 首先看一下需求
    • 要求可以创建多个倒计时器,可以暂停,以及恢复暂停。可以自由设置倒计时器总时间,倒计时间隔。下面会一步步实现一个多功能倒计时器。
  • 01.使用Handler实现倒计时
    • mHandler + runnable ,这种是最常见的一种方式。实质是不断调用mHandler.postDelayed(this, 1000)达到定时周期目的
  • 02.使用CountDownTimer实现倒计时
    • 也是利用mHandler + runnable,在此基础上简单封装一下。使用场景更强大,比如一个页面有多个倒计时器,用这个就很方便……
  • 03.利用Timer实现定时器
    • 使用Timer + TimerTask + handler方式实现倒计时
  • 04.使用chronometer控件倒计时
    • 新出的继承TextView组件,里头是使用了View.postDelayed + runnable实现倒计时
  • 05.利用动画实现倒计时
    • 这种方式用的比较少,但也是一种思路。主要是设置动画时间,在onAnimationUpdate监听设置倒计时处理
  • 具体代码案例可以看
  • 具体代码案例

02.各种倒计时器分析

  • 第一种利用Handler实现倒计时
    • 这种用的很普遍,但存在一个问题。如果是一个页面需要开启多个倒计时【比如列表页面】,则比较难处理。
  • 第二种使用CountDownTimer实现倒计时
    • new CountDownTimer(5000, 1000).start()
      • 期待的效果是:“5-4-3-2-1-finish”或者“5-4-3-2-1-0”。这里,显示 0 和 finish 的时间应该是一致的,所以把 0 放在 onFinish() 里显示也可以。但实际有误差……
    • 存在的几个问题
      • 问题1. 每次 onTick() 都会有几毫秒的误差,并不是期待的准确的 "5000, 4000, 3000, 2000, 1000, 0"。
      • 问题2. 多运行几次,就会发现这几毫秒的误差,导致了计算得出的剩余秒数并不准确,如果你的倒计时需要显示剩余秒数,就会发生 秒数跳跃/缺失 的情况(比如一开始从“4”开始显示——缺少“5”,或者直接从“5”跳到了“3”——缺少“4”)。
      • 问题3. 最后一次 onTick() 到 onFinish() 的间隔通常超过了 1 秒,差不多是 2 秒左右。如果你的倒计时在显示秒数,就能很明显的感觉到最后 1 秒停顿的时间很长。
      • 问题4. 如果onTick耗时超时,比如超过了1000毫秒,则会导致出现onTick出现跳动问题
    • 解决方案
      • 具体看lib中的CountDownTimer类。下面也会分析到
      • 注意:onTick方法中如何执行耗时操作【大于1秒的执行代码】,建议使用handler消息机制进行处理,避免出现其他问题。
  • 第三种利用Timer实现定时器
    • 注意点
      • Timer和TimerTask都有cancel方法,而且最好同时调用;如果已经cancel,下次必须创建新的Timer才能schedule。
    • 可能存在的问题
      • 如果你在当前的activity中schedule了一个task,但是没有等到task结束,就按Back键finish了当前的activity,Timer和TimerTask并不会自动cancel或者销毁,它还会在后台运行,此时如果你在task的某个阶段要调起一个控件(比如AlertDialog),而该控制依赖被销毁的activity,那么将会引发crash。
      • 所以建议在页面销毁的时候,将Timer和TimerTask都有cancel结束并且设置成null
      • Timer 的方式实现定时任务,用来做倒计时是没有问题的。但是如果用来执行周期任务,恰好又有多个任务,恰好两个任务之间的时间间隔又比前一个任务执行时间短就会发生定时不准确的现象了。Timer 在执行过程中如果任务跑出了异常,Timer 会停止所有的任务。Timer 执行周期任务时依赖系统时间,系统时间的变化会引起 Timer 任务执行的变化。

03.CountDownTimer解读

03.1 来看一个问题

  • 先看案例代码,如下所示
    • 期待的效果是:“5-4-3-2-1-finish”或者“5-4-3-2-1-0”。这里,显示 0 和 finish 的时间应该是一致的,所以把 0 放在 onFinish() 里显示也可以。
    mCountDownTimer = new CountDownTimer(5000, 1000) {
        @Override
        public void onTick(long millisUntilFinished) {
            Log.i(TAG, "----倒计时----onTick--"+millisUntilFinished);
        }
    
        public void onFinish() {
            Log.i(TAG, "----倒计时----onFinish");
        }
    };
    
  • 然后看一下打印日志,如下所示
    2020-08-05 10:04:28.742 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--5000
    2020-08-05 10:04:29.744 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--3998
    2020-08-05 10:04:30.746 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--2997
    2020-08-05 10:04:31.746 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--1996
    2020-08-05 10:04:32.747 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--995
    2020-08-05 10:04:33.747 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onFinish
    2020-08-05 10:04:45.397 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--4999
    2020-08-05 10:04:46.398 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--3998
    2020-08-05 10:04:47.400 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--2996
    2020-08-05 10:04:48.402 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--1994
    2020-08-05 10:04:49.405 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onTick--992
    2020-08-05 10:04:50.401 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒计时----onFinish
    
  • 可以看到有几个问题:
    • 问题1. 每次 onTick() 都会有几毫秒的误差,并不是期待的准确的 "5000, 4000, 3000, 2000, 1000, 0"。
    • 问题2. 多运行几次,就会发现这几毫秒的误差,导致了计算得出的剩余秒数并不准确,如果你的倒计时需要显示剩余秒数,就会发生 秒数跳跃/缺失 的情况(比如一开始从“4”开始显示——缺少“5”,或者直接从“5”跳到了“3”——缺少“4”)。
    • 问题3. 最后一次 onTick() 到 onFinish() 的间隔通常超过了 1 秒,差不多是 2 秒左右。如果你的倒计时在显示秒数,就能很明显的感觉到最后 1 秒停顿的时间很长。

03.3 分析时间误差

  • 为什么会存在这个问题
    • 先看start()方法,计算的 mStopTimeInFuture(未来停止倒计时的时刻,即倒计时结束时间) 加了一个 SystemClock.elapsedRealtime() ,系统自开机以来(包括睡眠时间)的毫秒数,也可以叫“系统时间戳”。
    • 即倒计时结束时间为“当前系统时间戳 + 你设置的倒计时时长 mMillisInFuture ”,也就是计算出的相对于手机系统开机以来的一个时间。在下面代码中打印日志看看
    public synchronized final void start() {
        if (mMillisInFuture <= 0 && mCountdownInterval <= 0) {
            throw new RuntimeException("you must set the millisInFuture > 0 or countdownInterval >0");
        }
        mCancelled = false;
        long elapsedRealtime = SystemClock.elapsedRealtime();
        mStopTimeInFuture = elapsedRealtime + mMillisInFuture;
        CountTimeTools.i("start → mMillisInFuture = " + mMillisInFuture + ", seconds = " + mMillisInFuture / 1000 );
        CountTimeTools.i("start → elapsedRealtime = " + elapsedRealtime + ", → mStopTimeInFuture = " + mStopTimeInFuture);
        mPause = false;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        if (mCountDownListener!=null){
            mCountDownListener.onStart();
        }
    }
    
    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
            synchronized (CountDownTimer.this) {
                if (mCancelled) {
                    return;
                }
                //剩余毫秒数
                final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
                if (millisLeft <= 0) {
                    mCurrentMillisLeft = 0;
                    if (mCountDownListener != null) {
                        mCountDownListener.onFinish();
                        CountTimeTools.i("onFinish → millisLeft = " + millisLeft);
                    }
                } else if (millisLeft < mCountdownInterval) {
                    mCurrentMillisLeft = 0;
                    CountTimeTools.i("handleMessage → millisLeft < mCountdownInterval !");
                    // 剩余时间小于一次时间间隔的时候,不再通知,只是延迟一下
                    sendMessageDelayed(obtainMessage(MSG), millisLeft);
                } else {
                    //有多余的时间
                    long lastTickStart = SystemClock.elapsedRealtime();
                    CountTimeTools.i("before onTick → lastTickStart = " + lastTickStart);
                    CountTimeTools.i("before onTick → millisLeft = " + millisLeft + ", seconds = " + millisLeft / 1000 );
                    if (mCountDownListener != null) {
                        mCountDownListener.onTick(millisLeft);
                        CountTimeTools.i("after onTick → elapsedRealtime = " + SystemClock.elapsedRealtime());
                    }
                    mCurrentMillisLeft = millisLeft;
                    // 考虑用户的onTick需要花费时间,处理用户onTick执行的时间
                    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                    CountTimeTools.i("after onTick → delay1 = " + delay);
                    // 特殊情况:用户的onTick方法花费的时间比interval长,那么直接跳转到下一次interval
                    // 注意,在onTick回调的方法中,不要做些耗时的操作
                    boolean isWhile = false;
                    while (delay < 0){
                        delay += mCountdownInterval;
                        isWhile = true;
                    }
                    if (isWhile){
                        CountTimeTools.i("after onTick执行超时 → delay2 = " + delay);
                    }
                    sendMessageDelayed(obtainMessage(MSG), delay);
                }
            }
        }
    };
    
  • 然后看一下日志
    2020-08-05 13:36:02.475 8742-8742/com.yc.yctimer I/CountDownTimer: start → mMillisInFuture = 5000, seconds = 5
    2020-08-05 13:36:02.475 8742-8742/com.yc.yctimer I/CountDownTimer: start → elapsedRealtime = 122669630, → mStopTimeInFuture = 122674630
    2020-08-05 13:36:02.478 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → lastTickStart = 122669634
    2020-08-05 13:36:02.478 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → millisLeft = 4996, seconds = 4
    2020-08-05 13:36:02.479 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → elapsedRealtime = 122669635
    2020-08-05 13:36:02.479 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → delay1 = 999
    2020-08-05 13:36:03.480 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → lastTickStart = 122670636
    2020-08-05 13:36:03.480 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → millisLeft = 3994, seconds = 3
    2020-08-05 13:36:03.483 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → elapsedRealtime = 122670639
    2020-08-05 13:36:03.484 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → delay1 = 996
    2020-08-05 13:36:04.482 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → lastTickStart = 122671638
    2020-08-05 13:36:04.483 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → millisLeft = 2992, seconds = 2
    2020-08-05 13:36:04.486 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → elapsedRealtime = 122671642
    2020-08-05 13:36:04.486 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → delay1 = 996
    2020-08-05 13:36:05.485 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → lastTickStart = 122672641
    2020-08-05 13:36:05.485 8742-8742/com.yc.yctimer I/CountDownTimer: before onTick → millisLeft = 1989, seconds = 1
    2020-08-05 13:36:05.488 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → elapsedRealtime = 122672644
    2020-08-05 13:36:05.488 8742-8742/com.yc.yctimer I/CountDownTimer: after onTick → delay1 = 997
    2020-08-05 13:36:06.487 8742-8742/com.yc.yctimer I/CountDownTimer: handleMessage → millisLeft < mCountdownInterval !
    2020-08-05 13:36:07.481 8742-8742/com.yc.yctimer I/CountDownTimer: onFinish → millisLeft = -3
    
  • 分析一下日志
    • 倒计时 5 秒,而 onTick() 一共只执行了 4 次。分别是出现4,3,2,1
    • start() 启动计时时,mMillisInFuture = 5000。且根据当前系统时间戳(记为 elapsedRealtime0 = 122669630,开始 start() 倒计时时的系统时间戳)计算了倒计时结束时相对于系统开机时的时间点 mStopTimeInFuture。
    • 此后到第一次进入 handleMessage() 时,中间经历了很短的时间 122669630 - 122669634 = 6 毫秒。
    • handleMessage() 这里精确计算了程序执行时间,虽然是第一次进入 handleMessage,也没有直接使用 mStopTimeInFuture,而是根据程序执行到此处时的 elapsedRealtime() (记为 elapsedRealtime1)来计算此时剩余的倒计时时长。
    • millisLeft = 4996,进入 else,执行 onTick()方法回调。所以第一次 onTick() 时,millisLeft = 4996,导致计算的剩余秒数是“4996/1000 = 4”,所以倒计时显示秒数是从“4”开始,而不是“5”开始。这便是前面提到的 问题1 和 问题2。
    • 考虑用户的onTick需要花费时间,处理用户onTick执行的时间,于是便发出一个延迟delay时间的消息sendMessageDelayed(obtainMessage(MSG), delay);在日志里看到delay1 = 997

03.3 onTick耗时超时

  • 上面分析到了用户的onTick需要花费时间,如果delay < 0则需要特殊处理,这个究竟是什么意思呢?下面来分析一下
  • 分析一下下面这个while循环作用
    // 考虑用户的onTick需要花费时间,处理用户onTick执行的时间
    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
    CountTimeTools.i("after onTick → delay1 = " + delay);
    // 特殊情况:用户的onTick方法花费的时间比interval长,那么直接跳转到下一次interval
    while (delay < 0){
        delay += mCountdownInterval;
    }
    CountTimeTools.i("after onTick → delay2 = " + delay);
    sendMessageDelayed(obtainMessage(MSG), delay);
    
    • 如果这次 onTick() 执行时间太长,超过了 mCountdownInterval ,那么执行完 onTick() 后计算得到的 delay 是一个负数,此时直接跳到下一次 mCountdownInterval 间隔,让 delay + mCountdownInterval。
  • 举一个例子来说一下,不然这里不太好理解
    • 假如设定每 1000 毫秒执行一次 onTick()。假设第一次 onTick() 开始前时的相对于手机系统开机时间的剩余倒计时时长是 5000 毫秒, 执行完这次 onTick() 操作消耗了 1015 毫秒,超出了我们设定的 1000 毫秒的间隔,那么第一次计算的 delay = 1000 - 1015 = -15 < 0,那么负数意味着什么呢?
    • 本来我们设定的 onTick() 调用间隔是 1000 毫秒,可是它执行完一次却用了 1015 毫秒,现在剩余倒计时还剩下 5000 - 1015 = 3985 毫秒,本来第二次 onTick() 按期望应该是在 4000 毫秒时开始执行的,可是此时第一次的 onTick() 却还未执行完。所以第二次 onTick() 就会被延迟 delay = -15 + 1000 = 985 毫秒,也就是到剩余 3000 毫秒时再执行了。
    • 那么此时就会 3985 / 1000 = 3,就会从5过度到3;依次类推,后续的delay延迟985毫秒后执行sendMessageDelayed,会导致时间出现跳跃性变动。具体可以看一下下面的例子……
  • onTick()做耗时操作会出现什么情况
    • 比如下面,看打印日志可知:4,2没有,这就意味着这个阶段没有执行到onTick()方法,而如果你在这个里有业务逻辑与时间节点有关,则可能会出现bug
    2020-08-05 13:58:00.657 11912-11912/com.yc.yctimer I/CountDownTimer: start → mMillisInFuture = 5000, seconds = 5
    2020-08-05 13:58:00.657 11912-11912/com.yc.yctimer I/CountDownTimer: start → elapsedRealtime = 123987813, → mStopTimeInFuture = 123992813
    2020-08-05 13:58:01.781 11912-11912/com.yc.yctimer I/CountDownTimer: before onTick → lastTickStart = 123988937
    2020-08-05 13:58:01.781 11912-11912/com.yc.yctimer I/CountDownTimer: before onTick → millisLeft = 3876, seconds = 3
    2020-08-05 13:58:02.858 11912-11912/com.yc.yctimer I/CountDownTimer: after onTick → elapsedRealtime = 123990014
    2020-08-05 13:58:02.858 11912-11912/com.yc.yctimer I/CountDownTimer: after onTick → delay1 = -77
    2020-08-05 13:58:02.858 11912-11912/com.yc.yctimer I/CountDownTimer: after onTick执行超时 → delay2 = 923
    2020-08-05 13:58:03.784 11912-11912/com.yc.yctimer I/CountDownTimer: before onTick → lastTickStart = 123990940
    2020-08-05 13:58:03.784 11912-11912/com.yc.yctimer I/CountDownTimer: before onTick → millisLeft = 1873, seconds = 1
    2020-08-05 13:58:04.896 11912-11912/com.yc.yctimer I/CountDownTimer: after onTick → elapsedRealtime = 123992052
    2020-08-05 13:58:04.896 11912-11912/com.yc.yctimer I/CountDownTimer: after onTick → delay1 = -112
    2020-08-05 13:58:04.896 11912-11912/com.yc.yctimer I/CountDownTimer: after onTick执行超时 → delay2 = 888
    2020-08-05 13:58:05.788 11912-11912/com.yc.yctimer I/CountDownTimer: onFinish → millisLeft = -130
    
  • onTick方法中如何执行耗时操作【大于1秒的执行代码】
    • 建议使用handler消息机制进行处理,避免出现其他问题。

03.4 代码改进完善

  • 针对 问题1 和 问题 2:
    • 问题描述
      • 问题1. 每次 onTick() 都会有几毫秒的误差,并不是期待的准确的 "5000, 4000, 3000, 2000, 1000, 0"。
      • 问题2. 多运行几次,就会发现这几毫秒的误差,导致了计算得出的剩余秒数并不准确,如果你的倒计时需要显示剩余秒数,就会发生 秒数跳跃/缺失 的情况(比如一开始从“4”开始显示——缺少“5”,或者直接从“5”跳到了“3”——缺少“4”)。
    • 解决方案
      • 这2个问题可以放在一起处理,网上也有很多人对这里做了改进,那就是给我们的 倒计时时长扩大一点点,通常是手动将 mMillisInFuture 扩大几十毫秒
    • 效果
      • 这里多加了 20 毫秒,运行一下(举个栗子)。倒计时打印日志:“5,4,3,2,1,finish”,

04.Timer和TimerTask解读

04.1 Timer和TimerTask方法

  • Timer核心方法如下所示
    //安排指定任务在指定时间执行。如果时间在过去,任务被安排立即执行。
    void schedule(TimerTask task, long delay)
    //将指定的任务调度为重复执行<i>固定延迟执行</i>,从指定的延迟开始。后续执行大约按按指定周期间隔的规则间隔进行。
    void schedule(TimerTask task, long delay, long period)
    
    • 第一个方法只执行一次;
    • 第二个方式每隔period执行一次,delay表示每次执行的延时时间,其实主要表现在第一次的延时效果,比如delay设置为0,那么立马执行task内容,如果设置为1000,那么第一次执行task会有一秒的延时效果。
  • TimerTask核心方法
    • TimerTask用于继承(或者直接定义并初始化匿名类),并重写run方法,定义自己的业务逻辑。
    //取消此计时器任务。如果任务被计划为一次性执行而尚未运行,或尚未被计划,则它将永远不会运行。
    //如果任务被安排为重复执行,它将永远不会再运行。(如果在此调用发生时任务正在运行,则任务将运行到完成,但将不再运行。)
    public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }
    
  • 关于结束定时器
    • Timer和TimerTask都有cancel方法,而且最好同时调用;如果已经cancel,下次必须创建新的Timer才能schedule。
    public void destroyTimer() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }
    
  • 可能存在的问题
    • 如果你在当前的activity中schedule了一个task,但是没有等到task结束,就按Back键finish了当前的activity,Timer和TimerTask并不会自动cancel或者销毁,它还会在后台运行,此时如果你在task的某个阶段要调起一个控件(比如AlertDialog),而该控制依赖被销毁的activity,那么将会引发crash。
    • 所以建议在页面销毁的时候,将Timer和TimerTask都有cancel结束并且设置成null
    • Timer 的方式实现定时任务,用来做倒计时是没有问题的。但是如果用来执行周期任务,恰好又有多个任务,恰好两个任务之间的时间间隔又比前一个任务执行时间短就会发生定时不准确的现象了。Timer 在执行过程中如果任务跑出了异常,Timer 会停止所有的任务。Timer 执行周期任务时依赖系统时间,系统时间的变化会引起 Timer 任务执行的变化。

04.2 Timer原理分析

  • 其基本处理模型是单线程调度的任务队列模型,Timer不停地接受调度任务,所有任务接受Timer调度后加入TaskQueue,TimerThread不停地去TaskQueue中取任务来执行。
  • 此种方式的不足之处为当某个任务执行时间较长,以致于超过了TaskQueue中下一个任务开始执行的时间,会影响整个任务执行的实时性。为了提高实时性,可以采用多个消费者一起消费来提高处理效率,避免此类问题的实现。

04.3 TimerTask分析

  • 源代码如下所示
    • 可以发现TimerTask是实现Runnable接口的一个抽象类。如果直接继承该类并且实现该类的run() 方法就可以了,里面包含这种对应的状态。
    public abstract class TimerTask implements Runnable {
        final Object lock = new Object();
        int state = VIRGIN;
        //表示尚未计划此任务(也表示初始状态)
        static final int VIRGIN = 0;
        //表示正在执行任务状态
        static final int SCHEDULED   = 1;
        //表示执行完成状态
        static final int EXECUTED    = 2;
        //取消状态
        static final int CANCELLED   = 3;
        //下次执行任务的时间
        long nextExecutionTime;
        //执行时间间隔
        long period = 0;
        //子类需要实现该方法,执行的任务的代码在该方法中实现
        public abstract void run();
        //取消任务,从这里我们可以很清楚知道取消任务就是修改状态
        public boolean cancel() {
            synchronized(lock) {
                boolean result = (state == SCHEDULED);
                state = CANCELLED;
                return result;
            }
        }
    }
    

04.4 Timer源码分析

  • Timer才是真正的核心,在创建Timer对象的同时也创建一个TimerThread对象,该类集成Thread,本质上就是开启了一个线程。
    public class Timer {
        //创建一个任务队列
        private final TaskQueue queue = new TaskQueue();
        //创建一个Thread线程对象,并且将queue队列传进去
        private final TimerThread thread = new TimerThread(queue);
        public Timer() {
            this("Timer-" + serialNumber());
        }
    
        public Timer(boolean isDaemon) {
            this("Timer-" + serialNumber(), isDaemon);
        }
    
        public Timer(String name) {
            thread.setName(name);
            thread.start();
        }
    
        public Timer(String name, boolean isDaemon) {
            thread.setName(name);
            thread.setDaemon(isDaemon);
            thread.start();
        }
    }
    
  • 然后看一下TimerThread线程的源码,如下所示
    • 首先看run方法中的mainLoop(),开启一个不断循环的线程如果队列中不存在任务则阻塞当前的线程,直到队列中添加任务以后唤醒线程。
    • 然后获取队列中执行时间最小的任务,如果该任务的状态是取消的话则从队列中移除掉再从队列中重新获取。
    • 最后判断当前的时间是否大于等于任务的执行的时间,如果任务的执行时间还未到则当前线程再阻塞一段时间,同时我们还要将该任务重新扔到任务队列中重新排序,我们必须保证队列中的第一个任务的执行时间是最小的。
    • 执行完mainLoop()方法完后,接着就将newTasksMayBeScheduled设置为false,并且清空队列中所有的任务。
    • 思考一下,这里的最小任务是什么意思?先把这个疑问记着……
    class TimerThread extends Thread {
        boolean newTasksMayBeScheduled = true;
        private TaskQueue queue;
    
        TimerThread(TaskQueue queue) {
            this.queue = queue;
        }
    
        public void run() {
            try {
                mainLoop();
            } finally {
                synchronized(queue) {
                //同时将状态置为false
                newTasksMayBeScheduled = false;
                //清空队列中所有的任务
                queue.clear();
            }
        }
    
        private void mainLoop() {
            //while死循环
            while (true) {
                try {
                    TimerTask task;
                    boolean taskFired;
                    synchronized(queue) {
                        //如果任务队列为空并且该标志位 true的话,则该线程一直进行等待中,直到队列中有任务进来的时候执行 queue.notify才会解除阻塞
                        while (queue.isEmpty() && newTasksMayBeScheduled)
                            queue.wait();
                        //如果队列中的内容为空的话直接跳出循环,外部调用者可能取消了Timer
                        if (queue.isEmpty())
                            break;
                        long currentTime, executionTime;
                        //获取队列中最近执行时间最小的任务(也就是最近需要执行的任务)
                        task = queue.getMin();
                        synchronized(task.lock) {
                            //如果该任务的状态是取消状态的话,那从队列中移除这个任务,然后继续执行循环队列操作
                            if (task.state == TimerTask.CANCELLED) {
                                queue.removeMin();
                                continue;
                            }
                            //获取当前系统时间
                            currentTime = System.currentTimeMillis();
                            //获取下一个目标要执行的时间
                            executionTime = task.nextExecutionTime;
                            //如果下一个目标要执行的时间大于等于等于时间了,表示要执行任务了
                            if (taskFired = (executionTime<=currentTime)) {
                                //如果task的时间间隔为0,表示只执行一次该任务
                                if (task.period == 0) {
                                    //将任务状态改为已执行状态,同时从队列中删除该任务
                                    queue.removeMin();
                                    task.state = TimerTask.EXECUTED;
                                } else {
                                    //将任务重新跟队列中的任务进行排列,要始终保证第一个task的时间是最小的
                                    queue.rescheduleMin(task.period<0 ? currentTime   - task.period
                                                    : executionTime + task.period);
                                }
                            }
                        }
                        //这里表示最近要执行的任务时间没有到,那么再让当前的线程阻塞一段时间
                        if (!taskFired)
                            queue.wait(executionTime - currentTime);
                    }
                    //表示要执行的任务时间已经到了,那么直接调用任务的run() 执行代码
                    if (taskFired)
                        task.run();
                } catch(InterruptedException e) {
                }
            }
        }
    }
    
  • 接着再来看一下TaskQueue队列的源代码
    • 可以发现这个队列使用数组实现的,如果超过了128的话则扩容为原来的两倍。这个代码不多,注释写的很详细了,没什么好讲的……
    public class TaskQueue {
        //创建一个数组为128的数组存放需要执行的任务,如果超过了128的话则扩容为原来的两倍
        private TimerTask[] queue = new TimerTask[128];
        //用于统计队列中任务的个数
        private int size = 0;
        //返回队列中任务的个数
        int size() {
            return size;
        }
    
        //依次遍历数组中的任务,并且置为null,有利于内存回收,注意这里的下标是从1开始计算的,不是从0
        void clear() {
            for (int i=1; i<=size; i++)
                queue[i] = null;
            size = 0;
        }
    
        //这里添加一个新的元素使用的是最小堆的操作,这里不详细说明了。
        void add(TimerTask task) {
            //如果数组已经存满任务,那么扩容一个新的数组为之前的两倍
            if (size + 1 == queue.length)
                queue = Arrays.copyOf(queue, 2*queue.length);
            queue[++size] = task;
            fixUp(size);
        }
    
        private void fixUp(int k) {
            while (k > 1) {
                int j = k >> 1;
                if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                    break;
                TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
                k = j;
            }
        }
    }
    

04.5 schedule发布任务

  • 当我们创建好Timer并且启动了循环线程以后,这个时候我们就需要发布任务。发布任务主要有以下几个方法。
    • schedule(TimerTask task, Date time)
      • 表示第一次执行任务的时间,时间间隔为0,也表示该任务只执行一次就结束了
    • schedule(TimerTask task, Date firstTime, long period)
      • firstTime 表示第一次执行的时间,period表示执行任务的时间间隔也就是多久时间执行一次
    • schedule(TimerTask task, long delay)
      • 延迟 delay时间执行任务,也就是在当前的时间+delay执行任务(该方法只执行一次任务)
  • 上面这三个方法都会执行sched方法,然后看一下这个
    • sched(TimerTask task, long time, long period)
      • 上面所有的执行任务的函数最后都是调用的该方法,task表示要执行的任务,time表示要执行任务的时间,period表示任务执行的间隔时间。
    • 具体看一下源代码
      private void sched(TimerTask task, long time, long period) {
          //如果时间间隔大于 long最大值的一般的话,需要对该数值 /2
          if (Math.abs(period) > (Long.MAX_VALUE >> 1))
              period >>= 1;
      
          synchronized(queue) {
              //首先判断轮训线程是否取消,如果取消状态直接抛出异常
              if (!thread.newTasksMayBeScheduled)
                  throw new IllegalStateException("Timer already cancelled.");
              synchronized(task.lock) {
                  //判断新执行的任务状态如果不是初始化状态话,直接抛出异常
                  if (task.state != TimerTask.VIRGIN)
                      throw new IllegalStateException("Task already scheduled or cancelled");
                  //赋值下次执行任务的时间
                  task.nextExecutionTime = time;
                  task.period = period;
                  //将任务状态修改为发布状态
                  task.state = TimerTask.SCHEDULED;
              }
              //将任务添加到最小堆队列中,注意:这里在添加到队列里面要保证第一个元素始终是最小的
              queue.add(task);
              //如果task就是队列中最小的任务话,则直接唤醒轮训线程执行任务(也就是唤醒TimerThread线程)
              if (queue.getMin() == task)
                  queue.notify();
          }
      }
      
    • 从上面的代码中可以清楚的明白发布任务非常简单的,就是往任务队列中添加任务然后判断条件是否需要唤醒轮训线程去执行任务。其核心代码是在 TimerThread 轮训中以及使用最小堆实现的队列保证每次取出来的第一个任务的执行时间是最小的。

04.6 存在的问题分析

  • Timer通过一个寻轮线程循环的从队列中获取需要执行的任务,如果任务的执行时间未到则进行等待(通过Object类的 wait 方法实现阻塞等待)一段时间再自动唤醒执行任务。
  • 但是细心的我们发现这个是单线程执行的如果有多个任务需要执行的话会不会应付不过来呢?类似一个程序员,要开发多个需求,要是所有的事情所耗费的时间很短的话,那么就不会出现延迟问题,要是其中一件或者是某件事情非常耗时间的话那么则会影响到后面事情的时间。
  • 其实这个现象一样跟Timer出现的问题也是一样的道理,如果某个任务非常耗时间,而且任务队列中的任务又比较多的话,那 TimerThread 是忙不过来的,这样子就会导致后面的任务出现延迟执行的问题,进而会影响所有的定时任务的准确执行时间。
  • 那么有人就会想要可以一个TimerTask对应一个Timer不就行了吗?但是我们要清楚的明白计算机的系统资源是有限的,如果我们一个任务就去单独的开一个轮训线程执行的话,其实是有一点浪费系统的资源的,完全没有必要的,如果不需要定时任务了话,我们还需要去销毁线程释放资源的,如果是这样子的反复操作的话,不利于我们程序的流畅性。

05.自定义倒计时器案例

  • 为了方便实现倒计时器自由灵活设置,且代码精简,能够适应一个页面创建多个定时器。或者用在列表中,同时倒计时器支持暂停,恢复倒计时等功能。这个就需要做特使处理呢。
    public class CountDownTimer {
    
        /**
         * 时间,即开始的时间,通俗来说就是倒计时总时间
         */
        private long mMillisInFuture;
        /**
         * 布尔值,表示计时器是否被取消
         * 只有调用cancel时才被设置为true
         */
        private boolean mCancelled = false;
        /**
         * 用户接收回调的时间间隔,一般是1秒
         */
        private long mCountdownInterval;
        /**
         * 记录暂停时候的时间
         */
        private long mStopTimeInFuture;
        /**
         * mas.what值
         */
        private static final int MSG = 520;
        /**
         * 暂停时,当时剩余时间
         */
        private long mCurrentMillisLeft;
        /**
         * 是否暂停
         * 只有当调用pause时,才设置为true
         */
        private boolean mPause = false;
        /**
         * 监听listener
         */
        private TimerListener mCountDownListener;
        /**
         * 是否创建开始
         */
        private boolean isStart;
    
        public CountDownTimer(){
            isStart = true;
        }
    
        public CountDownTimer(long millisInFuture, long countdownInterval) {
            long total = millisInFuture + 20;
            this.mMillisInFuture = total;
            //this.mMillisInFuture = millisInFuture;
            this.mCountdownInterval = countdownInterval;
            isStart = true;
        }
    
        /**
         * 开始倒计时,每次点击,都会重新开始
         */
        public synchronized final void start() {
            if (mMillisInFuture <= 0 && mCountdownInterval <= 0) {
                throw new RuntimeException("you must set the millisInFuture > 0 or countdownInterval >0");
            }
            mCancelled = false;
            long elapsedRealtime = SystemClock.elapsedRealtime();
            mStopTimeInFuture = elapsedRealtime + mMillisInFuture;
            CountTimeTools.i("start → mMillisInFuture = " + mMillisInFuture + ", seconds = " + mMillisInFuture / 1000 );
            CountTimeTools.i("start → elapsedRealtime = " + elapsedRealtime + ", → mStopTimeInFuture = " + mStopTimeInFuture);
            mPause = false;
            mHandler.sendMessage(mHandler.obtainMessage(MSG));
            if (mCountDownListener!=null){
                mCountDownListener.onStart();
            }
        }
    
        /**
         * 取消计时器
         */
        public synchronized final void cancel() {
            if (mHandler != null) {
                //暂停
                mPause = false;
                mHandler.removeMessages(MSG);
                //取消
                mCancelled = true;
            }
        }
    
        /**
         * 按一下暂停,再按一下继续倒计时
         */
        public synchronized final void pause() {
            if (mHandler != null) {
                if (mCancelled) {
                    return;
                }
                if (mCurrentMillisLeft < mCountdownInterval) {
                    return;
                }
                if (!mPause) {
                    mHandler.removeMessages(MSG);
                    mPause = true;
                }
            }
        }
    
        /**
         * 恢复暂停,开始
         */
        public synchronized final  void resume() {
            if (mMillisInFuture <= 0 && mCountdownInterval <= 0) {
                throw new RuntimeException("you must set the millisInFuture > 0 or countdownInterval >0");
            }
            if (mCancelled) {
                return;
            }
            //剩余时长少于
            if (mCurrentMillisLeft < mCountdownInterval || !mPause) {
                return;
            }
            mStopTimeInFuture = SystemClock.elapsedRealtime() + mCurrentMillisLeft;
            mHandler.sendMessage(mHandler.obtainMessage(MSG));
            mPause = false;
        }
    
    
        @SuppressLint("HandlerLeak")
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(@NonNull Message msg) {
                synchronized (CountDownTimer.this) {
                    if (mCancelled) {
                        return;
                    }
                    //剩余毫秒数
                    final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
                    if (millisLeft <= 0) {
                        mCurrentMillisLeft = 0;
                        if (mCountDownListener != null) {
                            mCountDownListener.onFinish();
                            CountTimeTools.i("onFinish → millisLeft = " + millisLeft);
                        }
                    } else if (millisLeft < mCountdownInterval) {
                        mCurrentMillisLeft = 0;
                        CountTimeTools.i("handleMessage → millisLeft < mCountdownInterval !");
                        // 剩余时间小于一次时间间隔的时候,不再通知,只是延迟一下
                        sendMessageDelayed(obtainMessage(MSG), millisLeft);
                    } else {
                        //有多余的时间
                        long lastTickStart = SystemClock.elapsedRealtime();
                        CountTimeTools.i("before onTick → lastTickStart = " + lastTickStart);
                        CountTimeTools.i("before onTick → millisLeft = " + millisLeft + ", seconds = " + millisLeft / 1000 );
                        if (mCountDownListener != null) {
                            mCountDownListener.onTick(millisLeft);
                            CountTimeTools.i("after onTick → elapsedRealtime = " + SystemClock.elapsedRealtime());
                        }
                        mCurrentMillisLeft = millisLeft;
                        // 考虑用户的onTick需要花费时间,处理用户onTick执行的时间
                        // 打印这个delay时间,大概是997毫秒
                        long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                        CountTimeTools.i("after onTick → delay1 = " + delay);
                        // 特殊情况:用户的onTick方法花费的时间比interval长,那么直接跳转到下一次interval
                        // 注意,在onTick回调的方法中,不要做些耗时的操作
                        boolean isWhile = false;
                        while (delay < 0){
                            delay += mCountdownInterval;
                            isWhile = true;
                        }
                        if (isWhile){
                            CountTimeTools.i("after onTick执行超时 → delay2 = " + delay);
                        }
                        sendMessageDelayed(obtainMessage(MSG), delay);
                    }
                }
            }
        };
    
        /**
         * 设置倒计时总时间
         * @param millisInFuture                    毫秒值
         */
        public void setMillisInFuture(long millisInFuture) {
            long total = millisInFuture + 20;
            this.mMillisInFuture = total;
        }
    
        /**
         * 设置倒计时间隔值
         * @param countdownInterval                 间隔,一般设置为1000毫秒
         */
        public void setCountdownInterval(long countdownInterval) {
            this.mCountdownInterval = countdownInterval;
        }
    
        /**
         * 设置倒计时监听
         * @param countDownListener                 listener
         */
        public void setCountDownListener(TimerListener countDownListener) {
            this.mCountDownListener = countDownListener;
        }
    
    }
    
  • 如何使用
    //开始
    mCountDownTimer.start();
    //结束销毁
    mCountDownTimer.cancel();
    //暂停
    mCountDownTimer.pause();
    //恢复暂停
    mCountDownTimer.resume();
    

代码案例:https://github.com/yangchong211/YCTimer

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342