<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          把倒計時做到極致,又準(zhǔn)、又穩(wěn)!

          共 10394字,需瀏覽 21分鐘

           ·

          2021-07-27 16:45

          相關(guān)閱讀:一個90后員工猝死的全過程


          快手電商無線團(tuán)隊-小政 | 作者
          承香墨影 | 編輯

          https://juejin.cn/post/6984725689257689101 | 原文


          能上架的 App,都逃不過一個倒計時的功能,手機驗證碼登錄總歸是要實現(xiàn)的。那這個功能中,實現(xiàn)的倒計時就包括每秒去修改 UI 倒計時計數(shù),讓時間的流逝做到用戶可感知。

          那么對于倒計時的功能,有那些要求呢?我認(rèn)為有 2 點:準(zhǔn) & 穩(wěn)

          準(zhǔn)是說,一個 2 分鐘的倒計時,就應(yīng)該嚴(yán)格執(zhí)行 2 分鐘,不會多 1 秒也不會少 1 秒;穩(wěn)的意思就是說,每次執(zhí)行同步 UI 的更新,間隔都是固定的,例如需要每秒 -1 的倒計時,那每次更新 UI 的時間,應(yīng)該都差不多是間隔 1秒。

          而在客戶端的環(huán)境下,有太多影響因素。比如 App 退出后臺并且鎖屏,導(dǎo)致 CPU 休眠了;又比如定時器每次喚醒的時間差了幾毫秒甚至幾十毫秒,倒計時結(jié)束時,總間隔時長大于預(yù)設(shè)的時長,這都是需要解決的問題。


          今天給大家推薦一篇來自快手電商無線團(tuán)隊-小政的文章,看快手是如何處理倒計時問題的,讓時間倒數(shù)的又準(zhǔn)又穩(wěn)。

          一、背景

          我們在項目中經(jīng)常有倒計時的場景,比如活動倒計時、搶紅包倒計時等等。通常情況下,我們實現(xiàn)倒計時的方案有 Android 中的 CountDownTimer、Java 中自帶的 Timer 和 ScheduleExcutorService、RxJava 中的 interval 操作符。

          在實際項目中存在 2 個典型的問題。

          1. 倒計時的實現(xiàn)形式不統(tǒng)一,不統(tǒng)一的原因分為認(rèn)知不一致、每種倒計時方案各有優(yōu)勢;
          2. 存在大量倒計時同時執(zhí)行。

          二、對比分析

          關(guān)于幾種方案的用法不是本文要討論的重點,在此我們通過表格的方式列出來各自的特性,表格底部的 CountDownTimerManager 就是本文要為大家介紹的新鮮出爐的中心化倒計時組件。

          2.1 是否是倒計時

          Rx 中的 interval 操作符,是每隔一段時間會發(fā)送一個事件,可以說是一個計數(shù)器,而不是倒計時,在實際項目中會發(fā)現(xiàn)很多同學(xué),都把它當(dāng)做倒計時在使用。下圖是 RxJava 官方對 interval 的圖解:

          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.(簡單理解就是固定間隔時間進(jìn)行回調(diào)

          通過源碼,我們也可以看出在 ObservableInterval 中實際也是進(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);

                worker.schedulePeriodically(is, initialDelay, period, unit);
              } else {

                Disposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit);
                is.setResource(d);
              }
            }
          }

          那么作為倒計時使用會有什么問題呢?

          • 問題一:回調(diào)可能不準(zhǔn)確,假設(shè)倒計時 9.5 秒,每 1 秒刷新一次 View,該怎么設(shè)置回調(diào)間隔時間呢?

          • 問題二:在手機長時間息屏后,某些廠商會將 CPU 休眠,RxJava 的 interval 操作符此時將被按下暫停鍵,當(dāng) APP 再次回到前臺,interval 會繼續(xù)執(zhí)行,假設(shè)暫停時倒計時剩余 100 秒,回到前臺后實際只有 10 秒了,但是 interval 還是從 100 繼續(xù)執(zhí)行。

          2.2 支持多任務(wù)

          Timer 是單線程串行執(zhí)行多任務(wù),假設(shè) taskA 設(shè)定 1 秒后執(zhí)行,taskB 設(shè)定 2 秒后執(zhí)行,實際上 taskB 是在 taskA 執(zhí)行結(jié)束后才執(zhí)行 taskB,所以 taskB 的執(zhí)行時間是在第 3 秒,所以 Timer 只算是偽支持多任務(wù)。ScheduledExecutorService 是利用線程池支持了多任務(wù)調(diào)度的。

          2.3 支持時間校準(zhǔn)

          CountDownTimer 中每次 onTick() 方法回調(diào),都會重新計算下一次 onTick 的時間。其中主要優(yōu)化有 2 點,一是減去 onTick() 執(zhí)行耗時;二是針對特殊情況(如 2.1 中提到的手機息屏后 CPU 休眠場景),對比 delay 是否小于 0,如果小于 0 則需要累加 mCountdownInterval

          long lastTickStart = SystemClock.elapsedRealtime();
          onTick(millisLeft);
          long lastTickDuration = SystemClock.elapsedRealtime() - lastTickStart;
          long delay;
          if (millisLeft < mCountdownInterval) {

            delay = millisLeft - lastTickDuration;
            if (delay < 0) {
              delay = 0;
            } else {
              delay = mCountdownInterval - lastTickDuration;

              while (delay < 0) {
               delay += mCountdownInterval;
              }
            }
            sendMessageDelayed(obtainMessage(MSG), delay);
          }

          2.4 支持同幀刷新

          我們項目中有很多場景是這樣的:

          倒計時 A 先執(zhí)行,倒計時 B 后執(zhí)行,A 和 B 的倒計時結(jié)束時間是一致的,那么我們假設(shè)倒計時時間為 10 秒,每 1 秒刷新一次,A 在剩余 10 秒時執(zhí)行,B 在剩余 9.5 秒執(zhí)行,當(dāng)二者在同一頁面顯示時,就會刷新不一致,這個問題在我們新的倒計時組件中將得到解決,文章后面將會詳細(xì)說明。

          2.5 支持延遲執(zhí)行

          延遲 1 分鐘再執(zhí)行 10 秒的倒計時?Android 中提供的 CountDownTimer 是做不到的,只能額外寫一個 1 分鐘的定時器,到時間后再啟動倒計時。

          2.6 支持 CPU 休眠

          我們這里提到的支持 CPU 休眠,并不是指 CPU 休眠期間倒計時仍能得到執(zhí)行,而是在 CPU 休眠后能夠恢復(fù)正常執(zhí)行。和 1.2.3 中提到的時間校準(zhǔn)類似,解決了時間校準(zhǔn)的問題也就支持了 CPU 休眠的特性。

          三、需求目標(biāo)

          1. 設(shè)計一個中心化的倒計時組件,同時支持上述提到的一系列特性;
          2. 接口易于調(diào)用,使用者只需關(guān)注計時回調(diào)的邏輯;

          四、設(shè)計類結(jié)構(gòu)

          CountDownTimer 采用靜態(tài)內(nèi)部類形式實現(xiàn)單例,暴露 countdown()timer() 方法供業(yè)務(wù)方 ClientA/ClientB/ClientC 等調(diào)用,Task 是抽象任務(wù),每次調(diào)用 countdown()timer() 后都生成一個 task,交給優(yōu)先級隊列管理,內(nèi)部通過 handler 不斷從隊列中取 task 執(zhí)行。

          五、具體實現(xiàn)

          5.1 收口

          收口可以理解為進(jìn)行統(tǒng)一管理,這里我們通過一個優(yōu)先級隊列管理所有倒計時、定時器,優(yōu)先級隊列可以直接采用 Java 中已有的數(shù)據(jù)結(jié)構(gòu) PriorityQueue,設(shè)置隊列大小默認(rèn)為 5,根據(jù) task 中的 mExecuteTimeInNext 進(jìn)行正序排序。

          這里有一個特別需要注意的點,PriorityQueue 需要傳入實現(xiàn) Comparator 接口的對象,在實現(xiàn) Comparator 時,因為 mExecuteTimeInNext 的數(shù)據(jù)類型是 long 類型,而 compare() 方法返回的是 int 類型,如果直接使用二者相減再強制轉(zhuǎn)換為 int,會有溢出的風(fēng)險,所以可以使用 Long.compare() 來實現(xiàn)大小比較。

          private final Queue<Task> mTaskQueue = new PriorityQueue<>(DEFAULT_INITIAL_CAPACITY,
            new Comparator<Task>() {
              @Override
              public int compare(Task task1, Task task2) {
                return Long.compare(task1.mExecuteTimeInNext, task2.mExecuteTimeInNext);
              }
            });

          5.2 支持與 RxJava 協(xié)同

          提供倒計時countdown、定時器 timer 操作符,直接返回 Observable,方便與 RxJava 框架協(xié)同。

            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();
              }
            });
          }
          public synchronized Observable<Long> timer(long millisInFuture) {
            return countdown(00, millisInFuture);
          }

          private synchronized void remove(Task task) {
            mTaskQueue.remove(task);
            if (mTaskQueue.size() == 0) {
              cancel();
            }
          }

          5.3 支持時間校準(zhǔn)

          不推薦使用 RxJava 中的 interval,因為 RxJava 中的實現(xiàn)無法保障倒計時的準(zhǔn)確執(zhí)行,如在手機 CPU 進(jìn)入休眠之后再恢復(fù)到前臺。那么如何實現(xiàn)呢?這里借鑒了 Android 中 CountDownTimer 的設(shè)計思路,在每次 onTick 后重新計算了下一次 onTick 的時間,比如前文提到的 “CPU 進(jìn)入休眠” 的情況,我們通過一個 while 循環(huán),計算出下一次 onTick 的時間(其條件是大于當(dāng)前時間)。

          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);

              do {
                task.mExecuteTimeInNext += task.mCountdownInterval;
              } while (task.mExecuteTimeInNext < SystemClock.elapsedRealtime());
              mTaskQueue.offer(task);
            }
          }

          5.4 支持同步刷新

          針對多個倒計時在同一時刻結(jié)束的情況,優(yōu)化了刷新不同步的問題。mExecuteTimeInNext 是下一次任務(wù)執(zhí)行時間,假設(shè)倒計時剩余時間為 9.5 秒,每 1 秒刷新,那么下一次的執(zhí)行時間則是在 0.5 秒之后。

          private Task(long millisInFuture, long countDownInterval, long delayMillis,
              @NonNull ObservableEmitter<Long> emitter)
           
          {
              
            mCountdownInterval = countDownInterval;
            mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
                : millisInFuture % mCountdownInterval) + delayMillis;
            mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
            mEmitter = emitter;
          }

          5.5 支持延遲執(zhí)行

          在計算下次執(zhí)行的時間時,加上了 delayMillis,這樣就支持了延遲執(zhí)行。

          private Task(long millisInFuture, long countDownInterval, long delayMillis,
              @NonNull ObservableEmitter<Long> emitter)
           
          {
            mCountdownInterval = countDownInterval;

            mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
                : millisInFuture % mCountdownInterval) + delayMillis;
            mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
            mEmitter = emitter;
          }

          六、小結(jié)

          文內(nèi)邏輯清晰,核心代碼都在文中,有需要自己復(fù)制出來改改就能用。

          另外我看到有人比較關(guān)心如何測試 CPU 休眠這個場景,CPU 是否進(jìn)入休眠,完全取決于 OS 的策略。

          不過有個步驟可以試試,將 App 置為后臺,然后鎖屏,在現(xiàn)在續(xù)航優(yōu)化的大環(huán)境下,多數(shù)手機廠商對電池的優(yōu)化手段,都會在這個場景,進(jìn)入 CPU 休眠狀態(tài),如果還不行,可以將手機設(shè)為「省電模式」。



          1、滴滴、滿幫、Boss直聘都被調(diào)查,為啥知乎美國上市沒被查?

          2、字節(jié)跳動重大宣布:取消!員工炸了:直接降薪1

          3、再見了,Teamviewer!

          4、人臉識別的時候,一定要穿上衣服啊!

          5、程序員被公司辭退12天,前領(lǐng)導(dǎo)要求回公司講清楚代碼,結(jié)果懵了

          瀏覽 101
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青草娱乐视频 | 九一网站平台直接观看 | 嘛豆三级片电影 | 免费看一级黄色电影 | 91人妻无码精品一区二区 |