复刻快手电商团队文章Android倒计时组件设计

1,072 阅读2分钟

公司项目中有大量的倒计时,刚好看到了快手电商无限团队的 一起设计一个Android倒计时组件 这篇文章感觉甚好,没有开源出来,评论区掘友们也在求,所以开源一下,致敬快手电商团队分享知识。

测试过程与效果

分别开启三个间隔为500L、2000L、1000L的倒计时,测试时间为 10 * 1000L。

    manager.countdown(millisInFuture, 500L, 0L).subscribe(new Observer<Long>() {
        @Override
        public void onSubscribe(@NonNull Disposable d) {
            Log.d("CountDownTimer1", "test countdown");
        }
        @Override
        public void onNext(@NonNull Long value) {
            Log.d("CountDownTimer1", "countdown Remaining time1: " + value);
        }
        @Override
        public void onComplete() {
            Log.d("CountDownTimer1", "countdown end1");
        }
    });
    manager.countdown(millisInFuture, 2000L, 0L).subscribe(new Observer<Long>() {
        @Override
        public void onSubscribe(@NonNull Disposable d) {
            Log.d("CountDownTimer2", "test countdown");
        }
        @Override
        public void onNext(@NonNull Long value) {
            Log.d("CountDownTimer2", "countdown Remaining time2: " + value);
        }
        @Override
        public void onComplete() {
            Log.d("CountDownTimer2", "countdown end2");
        }
    });
    manager.countdown(millisInFuture, 1000L, 0L).subscribe(new Observer<Long>() {
        @Override
        public void onSubscribe(@NonNull Disposable d) {
            Log.d("CountDownTimer3", "test countdown");
        }
        @Override
        public void onNext(@NonNull Long value) {
            Log.d("CountDownTimer3", "countdown Remaining time3: " + value);
        }
        @Override
        public void onComplete() {
            Log.d("CountDownTimer3", "countdown end3");
        }
    });

1681009576724.png

1681009551231.png

目前来看应该(大概)修复了长时间定时存在偏差的原因,之前写的版本跑一个小时,中间的时间损耗会呈现一个周期变化,直到大于每次的间隔时间;现在这个版本跑一个小时没有问题,解决时间损耗,能把精度控制在 5ms 内,基于 Handler 机制设计是这样的。

官方的 CountDownTimer 也是基于 Handler 机制,Handler 机制中的 Message 的触发时间只是表示该 Message 会在大于等于触发时间的时机触发。

实现过程

有试过用 ChatGPT 来复现,结果给的代码一大堆 Bug。

在自己实现过程中也碰到了问题,感谢 奶盖配红茶 大佬指点。

至于暂停和恢复功能,有兴趣的朋友可以自己加上。

Kotlin 版本的话用 SharedFlow 和 StateFlow 改造都可以。

源码设计

具体设计与思想请看快手文章,上面有链接。

dependencies {
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
    implementation 'io.reactivex.rxjava3:rxjava:3.0.0'
}
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.util.Log;

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.concurrent.atomic.AtomicReference;

import io.reactivex.rxjava3.annotations.NonNull;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;


public class CountDownTimerManager {

    private CountDownTimerManager() {}

    public static CountDownTimerManager getInstance() {
        return SingletonHolder.sInstance;
    }

    private static class SingletonHolder {
        private static final CountDownTimerManager sInstance = new CountDownTimerManager();
    }

    private static final Integer DEFAULT_INITIAL_CAPACITY = 5;
    private static final int MSG = 1;

    private boolean mIsCancelled = false;

    /**
     * 任务队列,按照 {@link Task#mExecuteTimeInNext} 时间排序
     */

    private Comparator<? super Task> getComparator() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Comparator.comparingLong((Task o) -> o.mExecuteTimeInNext);
        }
        return (Comparator<Task>) (o1, o2) -> Long.compare(o1.mExecuteTimeInNext,o2.mExecuteTimeInNext);
    }

    private final PriorityQueue<Task> mTaskQueue = new PriorityQueue<>(DEFAULT_INITIAL_CAPACITY, getComparator());

    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            synchronized (CountDownTimerManager.this) {
                if (mIsCancelled) {
                    return;
                }
                Task task = mTaskQueue.poll();
                if (task != null && !task.isDisposed()) {
                    if (task.mMillisUntilFinished < task.mCountdownInterval || task.mCountdownInterval == 0) {
                        task.dispose();
                    } else {
                        task.mEmitter.onNext(task.mMillisUntilFinished);
                        // 更新剩余时间
                        task.mMillisUntilFinished = task.mMillisUntilFinished - task.mCountdownInterval;
                        // 更新下一次执行时间
                        task.mExecuteTimeInNext += task.mCountdownInterval;
                        // 下一个 MSG 执行时间
                        long mNextMsgCountDownInterval = task.mExecuteTimeInNext - SystemClock.elapsedRealtime();
                        while (mNextMsgCountDownInterval < 0) {
                            mNextMsgCountDownInterval += task.mCountdownInterval;
                        }
                        long diff = Math.abs(mNextMsgCountDownInterval - task.mCountdownInterval);
                        if(diff >= 10) {
                            Log.d("CountDownTimerManager","下一次触发时间 偏差大于10:" + mNextMsgCountDownInterval);
                        }
                        mTaskQueue.offer(task);
                        final Message nextMsg = mHandler.obtainMessage(MSG);
                        mHandler.sendMessageDelayed(nextMsg, Math.max(10, mNextMsgCountDownInterval));
                    }
                }
            }
        }
    };

    public synchronized Observable<Long> countdown(long millisInFuture, long countDownInterval, long delayMillis) {
        AtomicReference<Task> taskAtomicReference = new AtomicReference<>();
        return Observable.create((ObservableOnSubscribe<Long>) emitter -> {
            if (millisInFuture <= 0) {
                emitter.onError(new IllegalArgumentException("millisInFuture must be greater than 0"));
            }
            if (countDownInterval < 0) {
                emitter.onError(new IllegalArgumentException("countDownInterval must be greater than or equal to 0"));
            }
            Task newTask = new Task(millisInFuture, countDownInterval, delayMillis, emitter);
            Task topTask = mTaskQueue.peek();
            if (topTask == null || newTask.mExecuteTimeInNext < topTask.mExecuteTimeInNext) {
                cancel();
            }
            mTaskQueue.offer(newTask);
            mIsCancelled = false;
            mHandler.sendMessage(mHandler.obtainMessage(MSG));
        }).doOnDispose(() -> {
            if (taskAtomicReference.get() != null) {
                taskAtomicReference.get().dispose();
            }
        });
    }

    public synchronized Observable<Long> timer(long delayMillis) {
        return countdown(0,0, delayMillis);
    }

    public synchronized void cancel() {
        mIsCancelled = true;
        mHandler.removeMessages(MSG);
    }

    private static class Task {
        // 是否结束
        boolean mDisposed;
        // 剩余时间
        long mMillisUntilFinished;
        // 间隔时间
        long mCountdownInterval;
        // 下次执行时间
        long mExecuteTimeInNext;
        // 结束时间
        long mStopTimeInFuture;

        ObservableEmitter<Long> mEmitter;

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

        void dispose() {
            if (!mDisposed && mEmitter != null) {
                mEmitter.onComplete();
                mEmitter = null;
                mDisposed = true;
            }
        }

        boolean isDisposed() {
            return mDisposed || mEmitter == null || mEmitter.isDisposed();
        }

    }
}