利用Android設(shè)計(jì)一個(gè)倒計(jì)時(shí)組件
1 背景
我們?cè)陧?xiàng)目中經(jīng)常有倒計(jì)時(shí)的場(chǎng)景,比如活動(dòng)倒計(jì)時(shí)、搶紅包倒計(jì)時(shí)等等。通常情況下,我們實(shí)現(xiàn)倒計(jì)時(shí)的方案有Android
中的CountDownTimer
、Java
中自帶的Timer
和ScheduleExcutorService
、RxJava
中的interval
操作符。 在實(shí)際項(xiàng)目中存在2個(gè)典型的問題,一是倒計(jì)時(shí)的實(shí)現(xiàn)形式不統(tǒng)一,不統(tǒng)一的原因分為認(rèn)知不一致、每種倒計(jì)時(shí)方案各有優(yōu)勢(shì);二是存在大量倒計(jì)時(shí)同時(shí)執(zhí)行。
2 對(duì)比分析
關(guān)于幾種方案的用法不是本文要討論的重點(diǎn),在此我們通過表格的方式列出來各自的特性,表格底部的CountDownTimerManager
就是本文要為大家介紹的新鮮出爐的中心化倒計(jì)時(shí)組件。
2.1 是否是倒計(jì)時(shí)
Rx中的interval
操作符是每隔一段時(shí)間會(huì)發(fā)送一個(gè)事件,可以說是一個(gè)計(jì)數(shù)器,而不是倒計(jì)時(shí),在實(shí)際項(xiàng)目中會(huì)發(fā)現(xiàn)很多同學(xué)都把它當(dāng)做倒計(jì)時(shí)在使用。下圖是RxJava
官方對(duì)interval
的圖解:
interval.png *The Interval operator returns an Observable that emits an infinite sequence of ascending integers, with a constant interval of time of your choosing between emissions.(簡(jiǎn)單理解就是固定間隔時(shí)間進(jìn)行回調(diào))
通過源碼,我們也可以看出在ObservableInterval
中實(shí)際也是進(jìn)行了周期性調(diào)度。
public final class ObservableInterval extends Observable<Long> { @Override public void subscribeActual(Observer<? super Long> observer) { IntervalObserver is = new IntervalObserver(observer); observer.onSubscribe(is); Scheduler sch = scheduler; if (sch instanceof TrampolineScheduler) { Worker worker = sch.createWorker(); is.setResource(worker); // 以給定的初始時(shí)間延遲、周期時(shí)間進(jìn)行周期性執(zhí)行 worker.schedulePeriodically(is, initialDelay, period, unit); } else { // 以給定的初始時(shí)間延遲、周期時(shí)間進(jìn)行周期性執(zhí)行 Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); is.setResource(d); } }
那么作為倒計(jì)時(shí)使用會(huì)有什么問題呢?
問題一是回調(diào)可能不準(zhǔn)確,假設(shè)倒計(jì)時(shí)9.5秒,每1秒刷新一次view,該怎么設(shè)置回調(diào)間隔時(shí)間呢?
問題二是在手機(jī)長時(shí)間息屏后,某些廠商會(huì)將CPU休眠,RxJava
的interval
操作符此時(shí)將被按下暫停鍵,當(dāng)APP再次回到前臺(tái),interval會(huì)繼續(xù)執(zhí)行,假設(shè)暫停時(shí)倒計(jì)時(shí)剩余100秒,回到前臺(tái)后實(shí)際只有10秒了,但是interval
還是從100繼續(xù)執(zhí)行。
2.2 支持多任務(wù)
Timer
是單線程串行執(zhí)行多任務(wù),假設(shè)taskA設(shè)定1秒后執(zhí)行,taskB設(shè)定2秒后執(zhí)行,實(shí)際上taskB是在taskA執(zhí)行結(jié)束后才執(zhí)行taskB,所以taskB的執(zhí)行時(shí)間是在第3秒,所以Timer
只算是偽支持多任務(wù)。ScheduledExecutorService
是利用線程池支持了多任務(wù)調(diào)度的。
2.3 支持時(shí)間校準(zhǔn)
CountDownTimer
中每次onTick()方法回調(diào),都會(huì)重新計(jì)算下一次onTick
的時(shí)間。其中主要優(yōu)化有2點(diǎn),一是減去onTick執(zhí)行耗時(shí);二是針對(duì)特殊情況(如1.2.1中提到的手機(jī)息屏后CPU休眠場(chǎng)景),對(duì)比delay
是否小于0,如果小于0則需要累加mCountdownInterval。
long lastTickStart = SystemClock.elapsedRealtime(); onTick(millisLeft); long lastTickDuration = SystemClock.elapsedRealtime() - lastTickStart; long delay; if (millisLeft < mCountdownInterval) { // 減去上面onTick方法執(zhí)行耗時(shí) delay = millisLeft - lastTickDuration; if (delay < 0) { delay = 0; } else { delay = mCountdownInterval - lastTickDuration; // 針對(duì)特殊情況(如1.2.1中提到的手機(jī)息屏后CPU休眠場(chǎng)景) // 對(duì)比delay是否小于0,如果小于0則需要累加mCountdownInterval while (delay < 0) { delay += mCountdownInterval; } } sendMessageDelayed(obtainMessage(MSG), delay); }
2.4 支持同幀刷新
我們項(xiàng)目中有很多場(chǎng)景是這樣的:
倒計(jì)時(shí)A先執(zhí)行,倒計(jì)時(shí)B后執(zhí)行,A和B的倒計(jì)時(shí)結(jié)束時(shí)間是一致的,那么我們假設(shè)倒計(jì)時(shí)時(shí)間為10秒,每1秒刷新一次,A在剩余10秒時(shí)執(zhí)行,B在剩余9.5秒執(zhí)行,當(dāng)二者在同一頁面顯示時(shí),就會(huì)刷新不一致,這個(gè)問題在我們新的倒計(jì)時(shí)組件中將得到解決,文章后面將會(huì)詳細(xì)說明。
2.5 支持延遲執(zhí)行
延遲1分鐘再執(zhí)行10秒的倒計(jì)時(shí)?Android
中提供的CountDownTimer
是做不到的,只能額外寫一個(gè)1分鐘的定時(shí)器,到時(shí)間后再啟動(dòng)倒計(jì)時(shí)。
2.6 支持CPU休眠
我們這里提到的支持CPU休眠,并不是指CPU休眠期間倒計(jì)時(shí)仍能得到執(zhí)行,而是在CPU休眠后能夠恢復(fù)正常執(zhí)行。和1.2.3中提到的時(shí)間校準(zhǔn)類似,解決了時(shí)間校準(zhǔn)的問題也就支持了CPU休眠的特性。
3 需求目標(biāo)
- 設(shè)計(jì)一個(gè)中心化的倒計(jì)時(shí)組件,同時(shí)支持上述提到的一系列特性。
- 接口易于調(diào)用,使用者只需關(guān)注計(jì)時(shí)回調(diào)的邏輯。
4 設(shè)計(jì)類結(jié)構(gòu)
CountDownTimer
采用靜態(tài)內(nèi)部類形式實(shí)現(xiàn)單例,暴露countdown()
、timer()
方法供業(yè)務(wù)方ClientA/ClientB/ClientC
等調(diào)用,Task是抽象任務(wù),每次調(diào)用countdown()
、timer()
后都生成一個(gè)task,交給優(yōu)先級(jí)隊(duì)列管理,內(nèi)部通過handler不斷從隊(duì)列中取task執(zhí)行。
5 具體實(shí)現(xiàn)
5.1 收口
收口可以理解為進(jìn)行統(tǒng)一管理,這里我們通過一個(gè)優(yōu)先級(jí)隊(duì)列管理所有倒計(jì)時(shí)、定時(shí)器,優(yōu)先級(jí)隊(duì)列可以直接采用Java中已有的數(shù)據(jù)結(jié)構(gòu)PriorityQueue
,設(shè)置隊(duì)列大小默認(rèn)為5,根據(jù)task中的mExecuteTimeInNext
進(jìn)行正序排序。這里有一個(gè)特別需要注意的點(diǎn),PriorityQueue
需要傳入實(shí)現(xiàn)Comparator
接口的對(duì)象,在實(shí)現(xiàn)Comparator
時(shí),因?yàn)?code>mExecuteTimeInNext的數(shù)據(jù)類型是long類型,而compare()
方法返回的是int類型,如果直接使用二者相減再強(qiáng)制轉(zhuǎn)換為int
,會(huì)有溢出的風(fēng)險(xiǎn),所以可以使用Long.compare()
來實(shí)現(xiàn)大小比較。
/** * 優(yōu)先級(jí)隊(duì)列,保存task,以 {@link Task#mExecuteTimeInNext} 作為基準(zhǔn) */ private final Queue<Task> mTaskQueue = new PriorityQueue<>(DEFAULT_INITIAL_CAPACITY, new Comparator<Task>() { @Override public int compare(Task task1, Task task2) { // return (int) (task1.mExecuteTimeInNext - task2.mExecuteTimeInNext); 錯(cuò)誤示范 return Long.compare(task1.mExecuteTimeInNext, task2.mExecuteTimeInNext); } });
5.2 支持與RxJava協(xié)同
提供倒計(jì)時(shí)countdown
、定時(shí)器timer
操作符,直接返回Observable
,方便與RxJava
框架協(xié)同。
/** * 倒計(jì)時(shí) * * @param millisInFuture Millis since epoch when alarm should stop. * @param countDownInterval The interval in millis that the user receives callbacks. * @param delayMillis The delay time in millis. * @return Observable */ public synchronized Observable<Long> countdown(long millisInFuture, long countDownInterval, long delayMillis) { AtomicReference<Task> taskAtomicReference = new AtomicReference<>(); return Observable.create((ObservableOnSubscribe<Long>) emitter -> { Task newTask = new Task(millisInFuture, countDownInterval, delayMillis, emitter); taskAtomicReference.set(newTask); synchronized (CountDownTimerManager.this) { Task topTask = mTaskQueue.peek(); if (topTask == null || newTask.mExecuteTimeInNext < topTask.mExecuteTimeInNext) { cancel(); } mTaskQueue.offer(newTask); if (mCancelled) { start(); } } }).doOnDispose(() -> { if (taskAtomicReference.get() != null) { taskAtomicReference.get().dispose(); } }); }
/** * 定時(shí)器 * * @param millisInFuture Millis since epoch when alarm should stop. * @return Observable */ public synchronized Observable<Long> timer(long millisInFuture) { return countdown(0, 0, millisInFuture); } private synchronized void remove(Task task) { mTaskQueue.remove(task); if (mTaskQueue.size() == 0) { cancel(); } }
5.3 支持時(shí)間校準(zhǔn)
不推薦使用RxJava
中的interval
,因?yàn)镽xJava中的實(shí)現(xiàn)無法保障倒計(jì)時(shí)的準(zhǔn)確執(zhí)行,如在手機(jī)CPU進(jìn)入休眠之后再恢復(fù)到前臺(tái)。那么如何實(shí)現(xiàn)呢?這里借鑒了Android
中CountDownTimer
的設(shè)計(jì)思路,在每次onTick后重新計(jì)算了下一次onTick的時(shí)間,比如前文提到的“CPU進(jìn)入休眠”的情況,我們通過一個(gè)while循環(huán),計(jì)算出下一次onTick的時(shí)間(其條件是大于當(dāng)前時(shí)間)。
mTaskQueue.poll(); if (!task.isDisposed()) { if (stopMillisLeft <= 0 || task.mCountdownInterval == 0) { task.mDisposed = true; task.mEmitter.onNext(0L); task.mEmitter.onComplete(); } else { task.mEmitter.onNext(stopMillisLeft % task.mCountdownInterval == 0 ? stopMillisLeft : (stopMillisLeft / task.mCountdownInterval + 1) * task.mCountdownInterval); // 時(shí)間校準(zhǔn) // special case: // user's onTick took more than interval to complete // cpu slept do { task.mExecuteTimeInNext += task.mCountdownInterval; } while (task.mExecuteTimeInNext < SystemClock.elapsedRealtime()); mTaskQueue.offer(task); } }
5.4 支持同步刷新
針對(duì)多個(gè)倒計(jì)時(shí)在同一時(shí)刻結(jié)束的情況,優(yōu)化了刷新不同步的問題。 mExecuteTimeInNext
是下一次任務(wù)執(zhí)行時(shí)間,假設(shè)倒計(jì)時(shí)剩余時(shí)間為9.5秒,每1秒刷新,那么下一次的執(zhí)行時(shí)間則是在0.5秒之后。
private Task(long millisInFuture, long countDownInterval, long delayMillis, @NonNull ObservableEmitter<Long> emitter) { mCountdownInterval = countDownInterval; // 計(jì)算出下次執(zhí)行的時(shí)間 mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0 : millisInFuture % mCountdownInterval) + delayMillis; mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis; mEmitter = emitter; }
5.5 支持延遲執(zhí)行
在計(jì)算下次執(zhí)行的時(shí)間時(shí),加上了delayMillis
,這樣就支持了延遲執(zhí)行。
private Task(long millisInFuture, long countDownInterval, long delayMillis, @NonNull ObservableEmitter<Long> emitter) { mCountdownInterval = countDownInterval; // 計(jì)算出下次執(zhí)行的時(shí)間 mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0 : millisInFuture % mCountdownInterval) + delayMillis; mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis; mEmitter = emitter; }
到此這篇關(guān)于利用Android設(shè)計(jì)一個(gè)倒計(jì)時(shí)組件的文章就介紹到這了,更多相關(guān)利用Android設(shè)計(jì)倒計(jì)時(shí)組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android自定義View實(shí)現(xiàn)隨機(jī)數(shù)驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了Android如何利用自定義View實(shí)現(xiàn)隨機(jī)數(shù)驗(yàn)證碼效果,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-06-06android全屏去掉title欄的多種實(shí)現(xiàn)方法
android全屏去掉title欄包括以下幾個(gè)部分:實(shí)現(xiàn)應(yīng)用中的所有activity都全屏/實(shí)現(xiàn)單個(gè)activity全屏/實(shí)現(xiàn)單個(gè)activity去掉title欄/自定義標(biāo)題內(nèi)容/自定義標(biāo)題布局等等感興趣的可參考下啊2013-02-02Android實(shí)現(xiàn)圖片輪播切換實(shí)例代碼
利用Android的ViewFlipper和AnimationUtils實(shí)現(xiàn)圖片帶有動(dòng)畫的輪播切換,其中當(dāng)點(diǎn)擊“上一張”圖片時(shí),切換到上一張圖片;當(dāng)點(diǎn)擊“下一張”圖片時(shí),切換到下一張圖片,本文給大家介紹Android實(shí)現(xiàn)圖片輪播切換實(shí)例代碼,需要的朋友參考下2015-12-12Android進(jìn)階從字節(jié)碼插樁技術(shù)了解美團(tuán)熱修復(fù)實(shí)例詳解
這篇文章主要為大家介紹了Android進(jìn)階從字節(jié)碼插樁技術(shù)了解美團(tuán)熱修復(fù)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Android開發(fā)之TextView使用intent傳遞信息,實(shí)現(xiàn)注冊(cè)界面功能示例
這篇文章主要介紹了Android開發(fā)之TextView使用intent傳遞信息,實(shí)現(xiàn)注冊(cè)界面功能,涉及Android使用intent傳值及界面布局等相關(guān)操作技巧,需要的朋友可以參考下2019-04-04android編程之下拉刷新實(shí)現(xiàn)方法分析
這篇文章主要介紹了android編程之下拉刷新實(shí)現(xiàn)方法,以實(shí)例形式詳細(xì)分析了Android編程中針對(duì)ListView下拉刷新的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11