1 background
We often have countdown scenes in the project, such as event countdown, red envelope countdown and so on. Generally, we implement CountDownTimer in Android, Timer and ScheduleExcutorService in Java, and interval operators in RxJava. There are two typical problems in the actual project. First, the realization form of the countdown is not unified. The reasons for the disunity are divided into cognitive inconsistency and each countdown scheme has its own advantages. Second, there are a large number of countdowns executed simultaneously.
2 Comparative Analysis
The usage of each of these solutions is not the focus of this article, but instead we list them in a table, with CountDownTimerManager at the bottom of the table as the new centralized countdown component for this article.
2.1 Countdown Or Not
The interval operator in Rx sends an event every once in a while, so it’s a counter, not a countdown, and you’ll see a lot of students using it as a countdown in real projects. Here is the official RxJava diagram for Interval:
*The Interval operator returns an Observable that emits an infinite sequence of ascending integers, with a constant interval of timeOf your choosing between emissions.
From the source code, we can also see that the ObservableInterval is actually scheduled periodically.
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);
// Execute periodically with the given initial time delay, cycle time
worker.schedulePeriodically(is, initialDelay, period, unit);
} else {
// Execute periodically with the given initial time delay, cycle timeDisposable d = sch.schedulePeriodicallyDirect(is, initialDelay, period, unit); is.setResource(d); }}Copy the code
So what’s the problem with using it as a countdown?
The first problem is that the callback may not be accurate. Assuming the countdown is 9.5 seconds and the view is refreshed every 1 second, how to set the callback interval?
The second problem is that some manufacturers will put the CPU to sleep after the screen has been shut down for a long time. The interval operator in RxJava will be paused. When the APP comes back to the foreground, the interval will continue to execute. But the interval continues at 100.
2.2 Support for multitasking
Timer is single-threaded serial execution multitasking, assuming taskA set 1 seconds after execution, taskB set 2 seconds after the execution, is actually executed until after taskA taskB is taskB, so taskB execution time is in 3 seconds, so that the Timer is only false support multitasking. ScheduledExecutorService supports multitask scheduling using thread pools.
2.3 Time calibration
Each onTick() callback in CountDownTimer recalculates the time of the next onTick. There are two main optimizations, one is to reduce onTick execution time; Second, for special cases (such as the CPU hibernation scenario mentioned in 1.2.1), compare whether the delay is less than 0. If the delay is less than 0, the mCountdownInterval needs to be accumulated.
long lastTickStart = SystemClock.elapsedRealtime();
onTick(millisLeft);
long lastTickDuration = SystemClock.elapsedRealtime() - lastTickStart;
long delay;
if (millisLeft < mCountdownInterval) {
// Subtracted the execution time of the onTick method above
delay = millisLeft - lastTickDuration;
if (delay < 0) {
delay = 0;
} else {
delay = mCountdownInterval - lastTickDuration;
// For special cases (such as the CPU hibernation scenario mentioned in 1.2.1)
If delay is less than 0, add mCountdownInterval
while (delay < 0) {
delay += mCountdownInterval;
}
}
sendMessageDelayed(obtainMessage(MSG), delay);
}
Copy the code
2.4 Refresh the same frame
Many of the scenarios in our project look like this:
Countdown to perform first, the countdown executed after B, A and B of the time the end of the countdown is consistent, so we assume that the countdown time for 10 seconds, refresh every 1 second, perform during the remaining 10 seconds, A B in 9.5 seconds remaining, when both on the same page refreshes, this problem will be solved in our new countdown components, More on this later in the article.
2.5 Delayed execution
Delay 1 minute and then execute the 10 second countdown? The CountDownTimer provided in Android doesn’t do this. You can only write an extra timer for 1 minute and then start the countdown after that time.
2.6 CPU hibernation
By enabling CPU sleep, we do not mean that the countdown can still execute during CPU sleep, but that it can resume normal execution after CPU sleep. Similar to the time calibration mentioned in 1.2.3, resolving the time calibration problem also supports the CPU sleep feature.
3 Requirement Objectives
● Design a centralized countdown component that supports all of the features mentioned above.
The interface is easy to call, and the consumer only needs to focus on the timing callback logic.
4 Design the class structure
CountDownTimer uses static inner class form to implement singleton, exposes countdown(), timer() method for business ClientA/ClientB/ClientC call, Task is abstract Task, After each call to Countdown () and timer(), a task is generated and handed to priority queue management. Internal handler is used to continuously fetch tasks from the queue for execution.
5 Concrete Implementation
5.1 convergent
In this case, we use a PriorityQueue to manage all the countdown and timer. The PriorityQueue can directly use the existing data structure PriorityQueue in Java, set the queue size to 5 by default, and sort positively according to mExecuteTimeInNext in task. One particular point to note here is that the PriorityQueue needs to be passed in an object that implements the Comparator interface. When Comparator is implemented, because mExecuteTimeInNext’s data type is long and compare() returns an int, There is a risk of overflow if you use the subtraction and cast to int directly, so you can use Long.compare() for size comparisons.
/** * priority queue, save task to {@linkTask#mExecuteTimeInNext} as a benchmark */
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); Error model
returnLong.compare(task1.mExecuteTimeInNext, task2.mExecuteTimeInNext); }});Copy the code
5.2 Supports collaboration with RxJava
Provide the countdown countdown, timer timer operator, directly return Observable, easy to work with the RxJava framework.
/** * countdown **@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(); }}); }Copy the code
/** * timer **@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(); }}Copy the code
5.3 Time calibration
Interval in RxJava is not recommended because the implementation in RxJava does not guarantee accurate countdown execution, such as returning to the foreground after the phone CPU has gone to sleep. So how does that work? Using the Android CountDownTimer, we recalculate the next onTick after each onTick. For example, in the “CPU goes to sleep” case, we use a while loop, Calculate the time for the next onTick (provided it is greater than the current time).
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);
// Time calibration
// 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); }}Copy the code
5.4 Refresh synchronously
Optimized the problem that the refresh is not synchronized when multiple countdowns end at the same time. MExecuteTimeInNext is the next time the task will be executed, assuming that the countdown clock is 9.5 seconds and refreshed every second, then the next time will be 0.5 seconds later.
private Task(long millisInFuture, long countDownInterval, long delayMillis,
@NonNull ObservableEmitter<Long> emitter) {
mCountdownInterval = countDownInterval;
// Calculate the next execution time
mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
: millisInFuture % mCountdownInterval) + delayMillis;
mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
mEmitter = emitter;
}
Copy the code
5.5 Delayed Execution
When calculating the time for the next execution, delayMillis is added to support delayed execution.
private Task(long millisInFuture, long countDownInterval, long delayMillis,
@NonNull ObservableEmitter<Long> emitter) {
mCountdownInterval = countDownInterval;
// Calculate the next execution time
mExecuteTimeInNext = SystemClock.elapsedRealtime() + (mCountdownInterval == 0 ? 0
: millisInFuture % mCountdownInterval) + delayMillis;
mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture + delayMillis;
mEmitter = emitter;
}
Copy the code
Hi, I’m Xiao Zheng from Kuaishou e-commerce
Kuaishou e-commerce wireless technology team is recruiting talents 🎉🎉🎉! We are the core business line of the company, here gathered all kinds of experts, but also full of opportunities and challenges. With the rapid development of the business, the team is also expanding rapidly. Welcome to join us and create world-class e-commerce products together
Hot positions: Android/iOS Senior Development, Android/iOS Expert, Java Architect, Product Manager (E-commerce background), Test Development… A lot of HC waiting for you ~
For internal recommendation, please send your resume to >>> our email: [email protected] <<<. Note that my roster success rate is higher ~ 😘