倒計(jì)時方案深入分析

目錄介紹

  • 01.使用多種方式實(shí)現(xiàn)倒計(jì)時
  • 02.各種倒計(jì)時器分析
  • 03.CountDownTimer解讀
  • 04.Timer和TimerTask解讀
  • 05.自定義倒計(jì)時器案例

01.使用多種方式實(shí)現(xiàn)倒計(jì)時

  • 首先看一下需求
    • 要求可以創(chuàng)建多個倒計(jì)時器霎奢,可以暫停,以及恢復(fù)暫停∽嵫撸可以自由設(shè)置倒計(jì)時器總時間,倒計(jì)時間隔舀患。下面會一步步實(shí)現(xiàn)一個多功能倒計(jì)時器滴劲。
  • 01.使用Handler實(shí)現(xiàn)倒計(jì)時
    • mHandler + runnable 底扳,這種是最常見的一種方式。實(shí)質(zhì)是不斷調(diào)用mHandler.postDelayed(this, 1000)達(dá)到定時周期目的
  • 02.使用CountDownTimer實(shí)現(xiàn)倒計(jì)時
    • 也是利用mHandler + runnable遏暴,在此基礎(chǔ)上簡單封裝一下侄刽。使用場景更強(qiáng)大,比如一個頁面有多個倒計(jì)時器朋凉,用這個就很方便……
  • 03.利用Timer實(shí)現(xiàn)定時器
    • 使用Timer + TimerTask + handler方式實(shí)現(xiàn)倒計(jì)時
  • 04.使用chronometer控件倒計(jì)時
    • 新出的繼承TextView組件州丹,里頭是使用了View.postDelayed + runnable實(shí)現(xiàn)倒計(jì)時
  • 05.利用動畫實(shí)現(xiàn)倒計(jì)時
    • 這種方式用的比較少,但也是一種思路。主要是設(shè)置動畫時間墓毒,在onAnimationUpdate監(jiān)聽設(shè)置倒計(jì)時處理
  • 具體代碼案例可以看
  • 具體代碼案例

02.各種倒計(jì)時器分析

  • 第一種利用Handler實(shí)現(xiàn)倒計(jì)時
    • 這種用的很普遍吓揪,但存在一個問題。如果是一個頁面需要開啟多個倒計(jì)時【比如列表頁面】所计,則比較難處理磺芭。
  • 第二種使用CountDownTimer實(shí)現(xiàn)倒計(jì)時
    • new CountDownTimer(5000, 1000).start()
      • 期待的效果是:“5-4-3-2-1-finish”或者“5-4-3-2-1-0”。這里醉箕,顯示 0 和 finish 的時間應(yīng)該是一致的钾腺,所以把 0 放在 onFinish() 里顯示也可以。但實(shí)際有誤差……
    • 存在的幾個問題
      • 問題1. 每次 onTick() 都會有幾毫秒的誤差讥裤,并不是期待的準(zhǔn)確的 "5000, 4000, 3000, 2000, 1000, 0"放棒。
      • 問題2. 多運(yùn)行幾次,就會發(fā)現(xiàn)這幾毫秒的誤差己英,導(dǎo)致了計(jì)算得出的剩余秒數(shù)并不準(zhǔn)確间螟,如果你的倒計(jì)時需要顯示剩余秒數(shù),就會發(fā)生 秒數(shù)跳躍/缺失 的情況(比如一開始從“4”開始顯示——缺少“5”损肛,或者直接從“5”跳到了“3”——缺少“4”)厢破。
      • 問題3. 最后一次 onTick() 到 onFinish() 的間隔通常超過了 1 秒,差不多是 2 秒左右治拿。如果你的倒計(jì)時在顯示秒數(shù)摩泪,就能很明顯的感覺到最后 1 秒停頓的時間很長。
      • 問題4. 如果onTick耗時超時劫谅,比如超過了1000毫秒见坑,則會導(dǎo)致出現(xiàn)onTick出現(xiàn)跳動問題
    • 解決方案
      • 具體看lib中的CountDownTimer類。下面也會分析到
      • 注意:onTick方法中如何執(zhí)行耗時操作【大于1秒的執(zhí)行代碼】捏检,建議使用handler消息機(jī)制進(jìn)行處理荞驴,避免出現(xiàn)其他問題。
  • 第三種利用Timer實(shí)現(xiàn)定時器
    • 注意點(diǎn)
      • Timer和TimerTask都有cancel方法贯城,而且最好同時調(diào)用熊楼;如果已經(jīng)cancel,下次必須創(chuàng)建新的Timer才能schedule能犯。
    • 可能存在的問題
      • 如果你在當(dāng)前的activity中schedule了一個task鲫骗,但是沒有等到task結(jié)束,就按Back鍵finish了當(dāng)前的activity悲雳,Timer和TimerTask并不會自動cancel或者銷毀挎峦,它還會在后臺運(yùn)行香追,此時如果你在task的某個階段要調(diào)起一個控件(比如AlertDialog)合瓢,而該控制依賴被銷毀的activity,那么將會引發(fā)crash透典。
      • 所以建議在頁面銷毀的時候晴楔,將Timer和TimerTask都有cancel結(jié)束并且設(shè)置成null
      • Timer 的方式實(shí)現(xiàn)定時任務(wù)顿苇,用來做倒計(jì)時是沒有問題的。但是如果用來執(zhí)行周期任務(wù)税弃,恰好又有多個任務(wù)纪岁,恰好兩個任務(wù)之間的時間間隔又比前一個任務(wù)執(zhí)行時間短就會發(fā)生定時不準(zhǔn)確的現(xiàn)象了。Timer 在執(zhí)行過程中如果任務(wù)跑出了異常则果,Timer 會停止所有的任務(wù)幔翰。Timer 執(zhí)行周期任務(wù)時依賴系統(tǒng)時間,系統(tǒng)時間的變化會引起 Timer 任務(wù)執(zhí)行的變化西壮。

03.CountDownTimer解讀

03.1 來看一個問題

  • 先看案例代碼遗增,如下所示
    • 期待的效果是:“5-4-3-2-1-finish”或者“5-4-3-2-1-0”。這里款青,顯示 0 和 finish 的時間應(yīng)該是一致的做修,所以把 0 放在 onFinish() 里顯示也可以。
    mCountDownTimer = new CountDownTimer(5000, 1000) {
        @Override
        public void onTick(long millisUntilFinished) {
            Log.i(TAG, "----倒計(jì)時----onTick--"+millisUntilFinished);
        }
    
        public void onFinish() {
            Log.i(TAG, "----倒計(jì)時----onFinish");
        }
    };
    
  • 然后看一下打印日志抡草,如下所示
    2020-08-05 10:04:28.742 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--5000
    2020-08-05 10:04:29.744 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--3998
    2020-08-05 10:04:30.746 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--2997
    2020-08-05 10:04:31.746 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--1996
    2020-08-05 10:04:32.747 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--995
    2020-08-05 10:04:33.747 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onFinish
    2020-08-05 10:04:45.397 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--4999
    2020-08-05 10:04:46.398 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--3998
    2020-08-05 10:04:47.400 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--2996
    2020-08-05 10:04:48.402 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--1994
    2020-08-05 10:04:49.405 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onTick--992
    2020-08-05 10:04:50.401 17266-17266/com.yc.yctimer I/CountDownTimer: ----倒計(jì)時----onFinish
    
  • 可以看到有幾個問題:
    • 問題1. 每次 onTick() 都會有幾毫秒的誤差饰及,并不是期待的準(zhǔn)確的 "5000, 4000, 3000, 2000, 1000, 0"。
    • 問題2. 多運(yùn)行幾次康震,就會發(fā)現(xiàn)這幾毫秒的誤差燎含,導(dǎo)致了計(jì)算得出的剩余秒數(shù)并不準(zhǔn)確,如果你的倒計(jì)時需要顯示剩余秒數(shù)腿短,就會發(fā)生 秒數(shù)跳躍/缺失 的情況(比如一開始從“4”開始顯示——缺少“5”瘫镇,或者直接從“5”跳到了“3”——缺少“4”)。
    • 問題3. 最后一次 onTick() 到 onFinish() 的間隔通常超過了 1 秒答姥,差不多是 2 秒左右铣除。如果你的倒計(jì)時在顯示秒數(shù),就能很明顯的感覺到最后 1 秒停頓的時間很長鹦付。

03.3 分析時間誤差

  • 為什么會存在這個問題
    • 先看start()方法尚粘,計(jì)算的 mStopTimeInFuture(未來停止倒計(jì)時的時刻,即倒計(jì)時結(jié)束時間) 加了一個 SystemClock.elapsedRealtime() 敲长,系統(tǒng)自開機(jī)以來(包括睡眠時間)的毫秒數(shù)郎嫁,也可以叫“系統(tǒng)時間戳”。
    • 即倒計(jì)時結(jié)束時間為“當(dāng)前系統(tǒng)時間戳 + 你設(shè)置的倒計(jì)時時長 mMillisInFuture ”祈噪,也就是計(jì)算出的相對于手機(jī)系統(tǒng)開機(jī)以來的一個時間泽铛。在下面代碼中打印日志看看
    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;
                }
                //剩余毫秒數(shù)
                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需要花費(fèi)時間,處理用戶onTick執(zhí)行的時間
                    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                    CountTimeTools.i("after onTick → delay1 = " + delay);
                    // 特殊情況:用戶的onTick方法花費(fèi)的時間比interval長盔腔,那么直接跳轉(zhuǎn)到下一次interval
                    // 注意,在onTick回調(diào)的方法中,不要做些耗時的操作
                    boolean isWhile = false;
                    while (delay < 0){
                        delay += mCountdownInterval;
                        isWhile = true;
                    }
                    if (isWhile){
                        CountTimeTools.i("after onTick執(zhí)行超時 → 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
    
  • 分析一下日志
    • 倒計(jì)時 5 秒弛随,而 onTick() 一共只執(zhí)行了 4 次瓢喉。分別是出現(xiàn)4,3舀透,2栓票,1
    • start() 啟動計(jì)時時,mMillisInFuture = 5000愕够。且根據(jù)當(dāng)前系統(tǒng)時間戳(記為 elapsedRealtime0 = 122669630走贪,開始 start() 倒計(jì)時時的系統(tǒng)時間戳)計(jì)算了倒計(jì)時結(jié)束時相對于系統(tǒng)開機(jī)時的時間點(diǎn) mStopTimeInFuture。
    • 此后到第一次進(jìn)入 handleMessage() 時惑芭,中間經(jīng)歷了很短的時間 122669630 - 122669634 = 6 毫秒厉斟。
    • handleMessage() 這里精確計(jì)算了程序執(zhí)行時間,雖然是第一次進(jìn)入 handleMessage强衡,也沒有直接使用 mStopTimeInFuture擦秽,而是根據(jù)程序執(zhí)行到此處時的 elapsedRealtime() (記為 elapsedRealtime1)來計(jì)算此時剩余的倒計(jì)時時長。
    • millisLeft = 4996漩勤,進(jìn)入 else感挥,執(zhí)行 onTick()方法回調(diào)。所以第一次 onTick() 時越败,millisLeft = 4996触幼,導(dǎo)致計(jì)算的剩余秒數(shù)是“4996/1000 = 4”,所以倒計(jì)時顯示秒數(shù)是從“4”開始究飞,而不是“5”開始置谦。這便是前面提到的 問題1 和 問題2。
    • 考慮用戶的onTick需要花費(fèi)時間亿傅,處理用戶onTick執(zhí)行的時間媒峡,于是便發(fā)出一個延遲delay時間的消息sendMessageDelayed(obtainMessage(MSG), delay);在日志里看到delay1 = 997

03.3 onTick耗時超時

  • 上面分析到了用戶的onTick需要花費(fèi)時間,如果delay < 0則需要特殊處理葵擎,這個究竟是什么意思呢谅阿?下面來分析一下
  • 分析一下下面這個while循環(huán)作用
    // 考慮用戶的onTick需要花費(fèi)時間,處理用戶onTick執(zhí)行的時間
    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
    CountTimeTools.i("after onTick → delay1 = " + delay);
    // 特殊情況:用戶的onTick方法花費(fèi)的時間比interval長,那么直接跳轉(zhuǎn)到下一次interval
    while (delay < 0){
        delay += mCountdownInterval;
    }
    CountTimeTools.i("after onTick → delay2 = " + delay);
    sendMessageDelayed(obtainMessage(MSG), delay);
    
    • 如果這次 onTick() 執(zhí)行時間太長酬滤,超過了 mCountdownInterval 签餐,那么執(zhí)行完 onTick() 后計(jì)算得到的 delay 是一個負(fù)數(shù),此時直接跳到下一次 mCountdownInterval 間隔盯串,讓 delay + mCountdownInterval氯檐。
  • 舉一個例子來說一下,不然這里不太好理解
    • 假如設(shè)定每 1000 毫秒執(zhí)行一次 onTick()体捏。假設(shè)第一次 onTick() 開始前時的相對于手機(jī)系統(tǒng)開機(jī)時間的剩余倒計(jì)時時長是 5000 毫秒冠摄, 執(zhí)行完這次 onTick() 操作消耗了 1015 毫秒糯崎,超出了我們設(shè)定的 1000 毫秒的間隔,那么第一次計(jì)算的 delay = 1000 - 1015 = -15 < 0耗拓,那么負(fù)數(shù)意味著什么呢?
    • 本來我們設(shè)定的 onTick() 調(diào)用間隔是 1000 毫秒奏司,可是它執(zhí)行完一次卻用了 1015 毫秒乔询,現(xiàn)在剩余倒計(jì)時還剩下 5000 - 1015 = 3985 毫秒,本來第二次 onTick() 按期望應(yīng)該是在 4000 毫秒時開始執(zhí)行的韵洋,可是此時第一次的 onTick() 卻還未執(zhí)行完竿刁。所以第二次 onTick() 就會被延遲 delay = -15 + 1000 = 985 毫秒,也就是到剩余 3000 毫秒時再執(zhí)行了搪缨。
    • 那么此時就會 3985 / 1000 = 3食拜,就會從5過度到3;依次類推副编,后續(xù)的delay延遲985毫秒后執(zhí)行sendMessageDelayed负甸,會導(dǎo)致時間出現(xiàn)跳躍性變動。具體可以看一下下面的例子……
  • onTick()做耗時操作會出現(xiàn)什么情況
    • 比如下面痹届,看打印日志可知:4呻待,2沒有,這就意味著這個階段沒有執(zhí)行到onTick()方法队腐,而如果你在這個里有業(yè)務(wù)邏輯與時間節(jié)點(diǎn)有關(guān)蚕捉,則可能會出現(xiàn)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執(zhí)行超時 → 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執(zhí)行超時 → delay2 = 888
    2020-08-05 13:58:05.788 11912-11912/com.yc.yctimer I/CountDownTimer: onFinish → millisLeft = -130
    
  • onTick方法中如何執(zhí)行耗時操作【大于1秒的執(zhí)行代碼】
    • 建議使用handler消息機(jī)制進(jìn)行處理,避免出現(xiàn)其他問題柴淘。

03.4 代碼改進(jìn)完善

  • 針對 問題1 和 問題 2:
    • 問題描述
      • 問題1. 每次 onTick() 都會有幾毫秒的誤差迫淹,并不是期待的準(zhǔn)確的 "5000, 4000, 3000, 2000, 1000, 0"。
      • 問題2. 多運(yùn)行幾次为严,就會發(fā)現(xiàn)這幾毫秒的誤差敛熬,導(dǎo)致了計(jì)算得出的剩余秒數(shù)并不準(zhǔn)確,如果你的倒計(jì)時需要顯示剩余秒數(shù)第股,就會發(fā)生 秒數(shù)跳躍/缺失 的情況(比如一開始從“4”開始顯示——缺少“5”荸型,或者直接從“5”跳到了“3”——缺少“4”)。
    • 解決方案
      • 這2個問題可以放在一起處理炸茧,網(wǎng)上也有很多人對這里做了改進(jìn)瑞妇,那就是給我們的 倒計(jì)時時長擴(kuò)大一點(diǎn)點(diǎn),通常是手動將 mMillisInFuture 擴(kuò)大幾十毫秒
    • 效果
      • 這里多加了 20 毫秒梭冠,運(yùn)行一下(舉個栗子)辕狰。倒計(jì)時打印日志:“5,4控漠,3蔓倍,2悬钳,1,finish”偶翅,

04.Timer和TimerTask解讀

04.1 Timer和TimerTask方法

  • Timer核心方法如下所示
    //安排指定任務(wù)在指定時間執(zhí)行默勾。如果時間在過去,任務(wù)被安排立即執(zhí)行聚谁。
    void schedule(TimerTask task, long delay)
    //將指定的任務(wù)調(diào)度為重復(fù)執(zhí)行<i>固定延遲執(zhí)行</i>母剥,從指定的延遲開始。后續(xù)執(zhí)行大約按按指定周期間隔的規(guī)則間隔進(jìn)行形导。
    void schedule(TimerTask task, long delay, long period)
    
    • 第一個方法只執(zhí)行一次环疼;
    • 第二個方式每隔period執(zhí)行一次,delay表示每次執(zhí)行的延時時間朵耕,其實(shí)主要表現(xiàn)在第一次的延時效果炫隶,比如delay設(shè)置為0,那么立馬執(zhí)行task內(nèi)容阎曹,如果設(shè)置為1000伪阶,那么第一次執(zhí)行task會有一秒的延時效果。
  • TimerTask核心方法
    • TimerTask用于繼承(或者直接定義并初始化匿名類)处嫌,并重寫run方法望门,定義自己的業(yè)務(wù)邏輯。
    //取消此計(jì)時器任務(wù)锰霜。如果任務(wù)被計(jì)劃為一次性執(zhí)行而尚未運(yùn)行筹误,或尚未被計(jì)劃,則它將永遠(yuǎn)不會運(yùn)行癣缅。
    //如果任務(wù)被安排為重復(fù)執(zhí)行厨剪,它將永遠(yuǎn)不會再運(yùn)行。(如果在此調(diào)用發(fā)生時任務(wù)正在運(yùn)行友存,則任務(wù)將運(yùn)行到完成祷膳,但將不再運(yùn)行。)
    public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }
    
  • 關(guān)于結(jié)束定時器
    • Timer和TimerTask都有cancel方法屡立,而且最好同時調(diào)用直晨;如果已經(jīng)cancel,下次必須創(chuàng)建新的Timer才能schedule膨俐。
    public void destroyTimer() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
            mTimerTask = null;
        }
    }
    
  • 可能存在的問題
    • 如果你在當(dāng)前的activity中schedule了一個task勇皇,但是沒有等到task結(jié)束,就按Back鍵finish了當(dāng)前的activity焚刺,Timer和TimerTask并不會自動cancel或者銷毀敛摘,它還會在后臺運(yùn)行,此時如果你在task的某個階段要調(diào)起一個控件(比如AlertDialog)乳愉,而該控制依賴被銷毀的activity兄淫,那么將會引發(fā)crash屯远。
    • 所以建議在頁面銷毀的時候,將Timer和TimerTask都有cancel結(jié)束并且設(shè)置成null
    • Timer 的方式實(shí)現(xiàn)定時任務(wù)捕虽,用來做倒計(jì)時是沒有問題的慨丐。但是如果用來執(zhí)行周期任務(wù),恰好又有多個任務(wù)泄私,恰好兩個任務(wù)之間的時間間隔又比前一個任務(wù)執(zhí)行時間短就會發(fā)生定時不準(zhǔn)確的現(xiàn)象了房揭。Timer 在執(zhí)行過程中如果任務(wù)跑出了異常,Timer 會停止所有的任務(wù)挖滤。Timer 執(zhí)行周期任務(wù)時依賴系統(tǒng)時間崩溪,系統(tǒng)時間的變化會引起 Timer 任務(wù)執(zhí)行的變化浅役。

04.2 Timer原理分析

  • 其基本處理模型是單線程調(diào)度的任務(wù)隊(duì)列模型斩松,Timer不停地接受調(diào)度任務(wù),所有任務(wù)接受Timer調(diào)度后加入TaskQueue,TimerThread不停地去TaskQueue中取任務(wù)來執(zhí)行觉既。
  • 此種方式的不足之處為當(dāng)某個任務(wù)執(zhí)行時間較長惧盹,以致于超過了TaskQueue中下一個任務(wù)開始執(zhí)行的時間,會影響整個任務(wù)執(zhí)行的實(shí)時性瞪讼。為了提高實(shí)時性钧椰,可以采用多個消費(fèi)者一起消費(fèi)來提高處理效率,避免此類問題的實(shí)現(xiàn)符欠。

04.3 TimerTask分析

  • 源代碼如下所示
    • 可以發(fā)現(xiàn)TimerTask是實(shí)現(xiàn)Runnable接口的一個抽象類嫡霞。如果直接繼承該類并且實(shí)現(xiàn)該類的run() 方法就可以了,里面包含這種對應(yīng)的狀態(tài)希柿。
    public abstract class TimerTask implements Runnable {
        final Object lock = new Object();
        int state = VIRGIN;
        //表示尚未計(jì)劃此任務(wù)(也表示初始狀態(tài))
        static final int VIRGIN = 0;
        //表示正在執(zhí)行任務(wù)狀態(tài)
        static final int SCHEDULED   = 1;
        //表示執(zhí)行完成狀態(tài)
        static final int EXECUTED    = 2;
        //取消狀態(tài)
        static final int CANCELLED   = 3;
        //下次執(zhí)行任務(wù)的時間
        long nextExecutionTime;
        //執(zhí)行時間間隔
        long period = 0;
        //子類需要實(shí)現(xiàn)該方法诊沪,執(zhí)行的任務(wù)的代碼在該方法中實(shí)現(xiàn)
        public abstract void run();
        //取消任務(wù),從這里我們可以很清楚知道取消任務(wù)就是修改狀態(tài)
        public boolean cancel() {
            synchronized(lock) {
                boolean result = (state == SCHEDULED);
                state = CANCELLED;
                return result;
            }
        }
    }
    

04.4 Timer源碼分析

  • Timer才是真正的核心曾撤,在創(chuàng)建Timer對象的同時也創(chuàng)建一個TimerThread對象端姚,該類集成Thread,本質(zhì)上就是開啟了一個線程挤悉。
    public class Timer {
        //創(chuàng)建一個任務(wù)隊(duì)列
        private final TaskQueue queue = new TaskQueue();
        //創(chuàng)建一個Thread線程對象渐裸,并且將queue隊(duì)列傳進(jìn)去
        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()装悲,開啟一個不斷循環(huán)的線程如果隊(duì)列中不存在任務(wù)則阻塞當(dāng)前的線程昏鹃,直到隊(duì)列中添加任務(wù)以后喚醒線程。
    • 然后獲取隊(duì)列中執(zhí)行時間最小的任務(wù)诀诊,如果該任務(wù)的狀態(tài)是取消的話則從隊(duì)列中移除掉再從隊(duì)列中重新獲取盆顾。
    • 最后判斷當(dāng)前的時間是否大于等于任務(wù)的執(zhí)行的時間,如果任務(wù)的執(zhí)行時間還未到則當(dāng)前線程再阻塞一段時間畏梆,同時我們還要將該任務(wù)重新扔到任務(wù)隊(duì)列中重新排序您宪,我們必須保證隊(duì)列中的第一個任務(wù)的執(zhí)行時間是最小的奈懒。
    • 執(zhí)行完mainLoop()方法完后,接著就將newTasksMayBeScheduled設(shè)置為false宪巨,并且清空隊(duì)列中所有的任務(wù)磷杏。
    • 思考一下,這里的最小任務(wù)是什么意思捏卓?先把這個疑問記著……
    class TimerThread extends Thread {
        boolean newTasksMayBeScheduled = true;
        private TaskQueue queue;
    
        TimerThread(TaskQueue queue) {
            this.queue = queue;
        }
    
        public void run() {
            try {
                mainLoop();
            } finally {
                synchronized(queue) {
                //同時將狀態(tài)置為false
                newTasksMayBeScheduled = false;
                //清空隊(duì)列中所有的任務(wù)
                queue.clear();
            }
        }
    
        private void mainLoop() {
            //while死循環(huán)
            while (true) {
                try {
                    TimerTask task;
                    boolean taskFired;
                    synchronized(queue) {
                        //如果任務(wù)隊(duì)列為空并且該標(biāo)志位 true的話极祸,則該線程一直進(jìn)行等待中,直到隊(duì)列中有任務(wù)進(jìn)來的時候執(zhí)行 queue.notify才會解除阻塞
                        while (queue.isEmpty() && newTasksMayBeScheduled)
                            queue.wait();
                        //如果隊(duì)列中的內(nèi)容為空的話直接跳出循環(huán)怠晴,外部調(diào)用者可能取消了Timer
                        if (queue.isEmpty())
                            break;
                        long currentTime, executionTime;
                        //獲取隊(duì)列中最近執(zhí)行時間最小的任務(wù)(也就是最近需要執(zhí)行的任務(wù))
                        task = queue.getMin();
                        synchronized(task.lock) {
                            //如果該任務(wù)的狀態(tài)是取消狀態(tài)的話遥金,那從隊(duì)列中移除這個任務(wù),然后繼續(xù)執(zhí)行循環(huán)隊(duì)列操作
                            if (task.state == TimerTask.CANCELLED) {
                                queue.removeMin();
                                continue;
                            }
                            //獲取當(dāng)前系統(tǒng)時間
                            currentTime = System.currentTimeMillis();
                            //獲取下一個目標(biāo)要執(zhí)行的時間
                            executionTime = task.nextExecutionTime;
                            //如果下一個目標(biāo)要執(zhí)行的時間大于等于等于時間了蒜田,表示要執(zhí)行任務(wù)了
                            if (taskFired = (executionTime<=currentTime)) {
                                //如果task的時間間隔為0稿械,表示只執(zhí)行一次該任務(wù)
                                if (task.period == 0) {
                                    //將任務(wù)狀態(tài)改為已執(zhí)行狀態(tài),同時從隊(duì)列中刪除該任務(wù)
                                    queue.removeMin();
                                    task.state = TimerTask.EXECUTED;
                                } else {
                                    //將任務(wù)重新跟隊(duì)列中的任務(wù)進(jìn)行排列冲粤,要始終保證第一個task的時間是最小的
                                    queue.rescheduleMin(task.period<0 ? currentTime   - task.period
                                                    : executionTime + task.period);
                                }
                            }
                        }
                        //這里表示最近要執(zhí)行的任務(wù)時間沒有到美莫,那么再讓當(dāng)前的線程阻塞一段時間
                        if (!taskFired)
                            queue.wait(executionTime - currentTime);
                    }
                    //表示要執(zhí)行的任務(wù)時間已經(jīng)到了,那么直接調(diào)用任務(wù)的run() 執(zhí)行代碼
                    if (taskFired)
                        task.run();
                } catch(InterruptedException e) {
                }
            }
        }
    }
    
  • 接著再來看一下TaskQueue隊(duì)列的源代碼
    • 可以發(fā)現(xiàn)這個隊(duì)列使用數(shù)組實(shí)現(xiàn)的梯捕,如果超過了128的話則擴(kuò)容為原來的兩倍厢呵。這個代碼不多,注釋寫的很詳細(xì)了傀顾,沒什么好講的……
    public class TaskQueue {
        //創(chuàng)建一個數(shù)組為128的數(shù)組存放需要執(zhí)行的任務(wù)襟铭,如果超過了128的話則擴(kuò)容為原來的兩倍
        private TimerTask[] queue = new TimerTask[128];
        //用于統(tǒng)計(jì)隊(duì)列中任務(wù)的個數(shù)
        private int size = 0;
        //返回隊(duì)列中任務(wù)的個數(shù)
        int size() {
            return size;
        }
    
        //依次遍歷數(shù)組中的任務(wù),并且置為null短曾,有利于內(nèi)存回收寒砖,注意這里的下標(biāo)是從1開始計(jì)算的,不是從0
        void clear() {
            for (int i=1; i<=size; i++)
                queue[i] = null;
            size = 0;
        }
    
        //這里添加一個新的元素使用的是最小堆的操作错英,這里不詳細(xì)說明了入撒。
        void add(TimerTask task) {
            //如果數(shù)組已經(jīng)存滿任務(wù),那么擴(kuò)容一個新的數(shù)組為之前的兩倍
            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發(fā)布任務(wù)

  • 當(dāng)我們創(chuàng)建好Timer并且啟動了循環(huán)線程以后椭岩,這個時候我們就需要發(fā)布任務(wù)茅逮。發(fā)布任務(wù)主要有以下幾個方法。
    • schedule(TimerTask task, Date time)
      • 表示第一次執(zhí)行任務(wù)的時間判哥,時間間隔為0献雅,也表示該任務(wù)只執(zhí)行一次就結(jié)束了
    • schedule(TimerTask task, Date firstTime, long period)
      • firstTime 表示第一次執(zhí)行的時間,period表示執(zhí)行任務(wù)的時間間隔也就是多久時間執(zhí)行一次
    • schedule(TimerTask task, long delay)
      • 延遲 delay時間執(zhí)行任務(wù)塌计,也就是在當(dāng)前的時間+delay執(zhí)行任務(wù)(該方法只執(zhí)行一次任務(wù))
  • 上面這三個方法都會執(zhí)行sched方法挺身,然后看一下這個
    • sched(TimerTask task, long time, long period)
      • 上面所有的執(zhí)行任務(wù)的函數(shù)最后都是調(diào)用的該方法,task表示要執(zhí)行的任務(wù)锌仅,time表示要執(zhí)行任務(wù)的時間章钾,period表示任務(wù)執(zhí)行的間隔時間墙贱。
    • 具體看一下源代碼
      private void sched(TimerTask task, long time, long period) {
          //如果時間間隔大于 long最大值的一般的話,需要對該數(shù)值 /2
          if (Math.abs(period) > (Long.MAX_VALUE >> 1))
              period >>= 1;
      
          synchronized(queue) {
              //首先判斷輪訓(xùn)線程是否取消贱傀,如果取消狀態(tài)直接拋出異常
              if (!thread.newTasksMayBeScheduled)
                  throw new IllegalStateException("Timer already cancelled.");
              synchronized(task.lock) {
                  //判斷新執(zhí)行的任務(wù)狀態(tài)如果不是初始化狀態(tài)話惨撇,直接拋出異常
                  if (task.state != TimerTask.VIRGIN)
                      throw new IllegalStateException("Task already scheduled or cancelled");
                  //賦值下次執(zhí)行任務(wù)的時間
                  task.nextExecutionTime = time;
                  task.period = period;
                  //將任務(wù)狀態(tài)修改為發(fā)布狀態(tài)
                  task.state = TimerTask.SCHEDULED;
              }
              //將任務(wù)添加到最小堆隊(duì)列中,注意:這里在添加到隊(duì)列里面要保證第一個元素始終是最小的
              queue.add(task);
              //如果task就是隊(duì)列中最小的任務(wù)話府寒,則直接喚醒輪訓(xùn)線程執(zhí)行任務(wù)(也就是喚醒TimerThread線程)
              if (queue.getMin() == task)
                  queue.notify();
          }
      }
      
    • 從上面的代碼中可以清楚的明白發(fā)布任務(wù)非常簡單的魁衙,就是往任務(wù)隊(duì)列中添加任務(wù)然后判斷條件是否需要喚醒輪訓(xùn)線程去執(zhí)行任務(wù)。其核心代碼是在 TimerThread 輪訓(xùn)中以及使用最小堆實(shí)現(xiàn)的隊(duì)列保證每次取出來的第一個任務(wù)的執(zhí)行時間是最小的株搔。

04.6 存在的問題分析

  • Timer通過一個尋輪線程循環(huán)的從隊(duì)列中獲取需要執(zhí)行的任務(wù)剖淀,如果任務(wù)的執(zhí)行時間未到則進(jìn)行等待(通過Object類的 wait 方法實(shí)現(xiàn)阻塞等待)一段時間再自動喚醒執(zhí)行任務(wù)。
  • 但是細(xì)心的我們發(fā)現(xiàn)這個是單線程執(zhí)行的如果有多個任務(wù)需要執(zhí)行的話會不會應(yīng)付不過來呢纤房?類似一個程序員纵隔,要開發(fā)多個需求,要是所有的事情所耗費(fèi)的時間很短的話帆卓,那么就不會出現(xiàn)延遲問題巨朦,要是其中一件或者是某件事情非常耗時間的話那么則會影響到后面事情的時間米丘。
  • 其實(shí)這個現(xiàn)象一樣跟Timer出現(xiàn)的問題也是一樣的道理剑令,如果某個任務(wù)非常耗時間,而且任務(wù)隊(duì)列中的任務(wù)又比較多的話拄查,那 TimerThread 是忙不過來的吁津,這樣子就會導(dǎo)致后面的任務(wù)出現(xiàn)延遲執(zhí)行的問題,進(jìn)而會影響所有的定時任務(wù)的準(zhǔn)確執(zhí)行時間堕扶。
  • 那么有人就會想要可以一個TimerTask對應(yīng)一個Timer不就行了嗎碍脏?但是我們要清楚的明白計(jì)算機(jī)的系統(tǒng)資源是有限的,如果我們一個任務(wù)就去單獨(dú)的開一個輪訓(xùn)線程執(zhí)行的話稍算,其實(shí)是有一點(diǎn)浪費(fèi)系統(tǒng)的資源的典尾,完全沒有必要的,如果不需要定時任務(wù)了話糊探,我們還需要去銷毀線程釋放資源的钾埂,如果是這樣子的反復(fù)操作的話,不利于我們程序的流暢性科平。

05.自定義倒計(jì)時器案例

  • 為了方便實(shí)現(xiàn)倒計(jì)時器自由靈活設(shè)置褥紫,且代碼精簡,能夠適應(yīng)一個頁面創(chuàng)建多個定時器瞪慧∷杩迹或者用在列表中,同時倒計(jì)時器支持暫停弃酌,恢復(fù)倒計(jì)時等功能氨菇。這個就需要做特使處理呢儡炼。
    public class CountDownTimer {
    
        /**
         * 時間,即開始的時間查蓉,通俗來說就是倒計(jì)時總時間
         */
        private long mMillisInFuture;
        /**
         * 布爾值射赛,表示計(jì)時器是否被取消
         * 只有調(diào)用cancel時才被設(shè)置為true
         */
        private boolean mCancelled = false;
        /**
         * 用戶接收回調(diào)的時間間隔,一般是1秒
         */
        private long mCountdownInterval;
        /**
         * 記錄暫停時候的時間
         */
        private long mStopTimeInFuture;
        /**
         * mas.what值
         */
        private static final int MSG = 520;
        /**
         * 暫停時奶是,當(dāng)時剩余時間
         */
        private long mCurrentMillisLeft;
        /**
         * 是否暫停
         * 只有當(dāng)調(diào)用pause時楣责,才設(shè)置為true
         */
        private boolean mPause = false;
        /**
         * 監(jiān)聽listener
         */
        private TimerListener mCountDownListener;
        /**
         * 是否創(chuàng)建開始
         */
        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;
        }
    
        /**
         * 開始倒計(jì)時,每次點(diǎn)擊聂沙,都會重新開始
         */
        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();
            }
        }
    
        /**
         * 取消計(jì)時器
         */
        public synchronized final void cancel() {
            if (mHandler != null) {
                //暫停
                mPause = false;
                mHandler.removeMessages(MSG);
                //取消
                mCancelled = true;
            }
        }
    
        /**
         * 按一下暫停秆麸,再按一下繼續(xù)倒計(jì)時
         */
        public synchronized final void pause() {
            if (mHandler != null) {
                if (mCancelled) {
                    return;
                }
                if (mCurrentMillisLeft < mCountdownInterval) {
                    return;
                }
                if (!mPause) {
                    mHandler.removeMessages(MSG);
                    mPause = true;
                }
            }
        }
    
        /**
         * 恢復(fù)暫停,開始
         */
        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;
                    }
                    //剩余毫秒數(shù)
                    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需要花費(fèi)時間,處理用戶onTick執(zhí)行的時間
                        // 打印這個delay時間,大概是997毫秒
                        long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                        CountTimeTools.i("after onTick → delay1 = " + delay);
                        // 特殊情況:用戶的onTick方法花費(fèi)的時間比interval長坷随,那么直接跳轉(zhuǎn)到下一次interval
                        // 注意房铭,在onTick回調(diào)的方法中,不要做些耗時的操作
                        boolean isWhile = false;
                        while (delay < 0){
                            delay += mCountdownInterval;
                            isWhile = true;
                        }
                        if (isWhile){
                            CountTimeTools.i("after onTick執(zhí)行超時 → delay2 = " + delay);
                        }
                        sendMessageDelayed(obtainMessage(MSG), delay);
                    }
                }
            }
        };
    
        /**
         * 設(shè)置倒計(jì)時總時間
         * @param millisInFuture                    毫秒值
         */
        public void setMillisInFuture(long millisInFuture) {
            long total = millisInFuture + 20;
            this.mMillisInFuture = total;
        }
    
        /**
         * 設(shè)置倒計(jì)時間隔值
         * @param countdownInterval                 間隔温眉,一般設(shè)置為1000毫秒
         */
        public void setCountdownInterval(long countdownInterval) {
            this.mCountdownInterval = countdownInterval;
        }
    
        /**
         * 設(shè)置倒計(jì)時監(jiān)聽
         * @param countDownListener                 listener
         */
        public void setCountDownListener(TimerListener countDownListener) {
            this.mCountDownListener = countDownListener;
        }
    
    }
    
  • 如何使用
    //開始
    mCountDownTimer.start();
    //結(jié)束銷毀
    mCountDownTimer.cancel();
    //暫停
    mCountDownTimer.pause();
    //恢復(fù)暫停
    mCountDownTimer.resume();
    

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缸匪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子类溢,更是在濱河造成了極大的恐慌凌蔬,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件闯冷,死亡現(xiàn)場離奇詭異砂心,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蛇耀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門辩诞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人纺涤,你說我怎么就攤上這事译暂。” “怎么了洒琢?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵秧秉,是天一觀的道長。 經(jīng)常有香客問我衰抑,道長象迎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮砾淌,結(jié)果婚禮上啦撮,老公的妹妹穿的比我還像新娘。我一直安慰自己汪厨,他們只是感情好赃春,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著劫乱,像睡著了一般织中。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衷戈,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天狭吼,我揣著相機(jī)與錄音,去河邊找鬼殖妇。 笑死刁笙,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谦趣。 我是一名探鬼主播疲吸,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼前鹅!你這毒婦竟也來了摘悴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤嫡纠,失蹤者是張志新(化名)和其女友劉穎烦租,沒想到半個月后延赌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體除盏,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡峰髓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年挣菲,在試婚紗的時候發(fā)現(xiàn)自己被綠了勤揩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片级乐。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡紧憾,死狀恐怖齐鲤,靈堂內(nèi)的尸體忽然破棺而出埠戳,到底是詐尸還是另有隱情趟畏,我是刑警寧澤大磺,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布抡句,位于F島的核電站,受9級特大地震影響杠愧,放射性物質(zhì)發(fā)生泄漏待榔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锐锣。 院中可真熱鬧腌闯,春花似錦、人聲如沸雕憔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斤彼。三九已至分瘦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間琉苇,已是汗流浹背擅腰。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翁潘,地道東北人趁冈。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像拜马,于是被迫代替她去往敵國和親渗勘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356