目錄介紹
- 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)其他問題。
- new CountDownTimer(5000, 1000).start()
- 第三種利用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í)行的變化西壮。
- 注意點(diǎn)
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ù))
- schedule(TimerTask task, Date time)
- 上面這三個方法都會執(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í)行時間是最小的株搔。
- sched(TimerTask task, long time, long period)
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();