In-depth analysis of countdown scheme

catalogue

  • 01. Use a variety of ways to achieve the countdown
  • 02. Analysis of all kinds of countdown timer
  • 03. CountDownTimer interpretation
  • 04. Interpreting Timer and TimerTask
  • 05. Custom timer case

01. Use a variety of ways to achieve the countdown

  • Let’s first look at the requirements
    • Requirements can create multiple countdown timers, can pause, and resume pause. You can freely set the total time of the countdown timer, countdown interval. The following will be a step by step to achieve a multifunctional timer.
  • 01. Use Handler to implement countdown
    • MHandler + Runnable, which is the most common method. Essentially, mHandler.postDelayed(this, 1000) is called repeatedly for the timed period purpose
  • 02. Use CountDownTimer to count down
    • Also use mHandler + Runnable, on this basis simple encapsulation. Using scenarios is more powerful, such as multiple countdown timers on a page, which is handy…
  • 03. Use Timer to implement the Timer
    • Use Timer + TimerTask + Handler to realize the countdown
  • 04. Use the Chronometer control to count down
    • A new inherited TextView component that uses view.postdelayed + runnable for the countdown
  • 05. Use animation to realize the countdown
    • This method is used less often, but it is a way of thinking. The main thing is to set the animation time. The onAnimationUpdate listener sets the countdown processing
  • Specific code cases can be seen
    • 6 code cases to implement a countdown timer
  • Specific code cases
    • 6 kinds of countdown schemes

02. Analysis of all kinds of countdown timer

  • The first uses a Handler to implement the countdown
    • It’s very common, but there’s a problem. If a page requires multiple countdowns (such as a list page), this can be difficult to handle.
  • The second uses CountDownTimer for the countdown
    • new CountDownTimer(5000, 1000).start()
      • Expected effect: ‘5-4-3-2-1-finish’ or ‘5-4-3-2-1-0’. In this case, 0 should be displayed at the same time as finish, so placing 0 in onFinish() is ok. But there are errors…
    • There are several problems
      • Problem 1. Each onTick() error is a few milliseconds, not the expected “5000, 4000, 3000, 2000, 1000, 0”.
      • Problem 2. If you run it a few more times, you’ll find that the remaining seconds are off by a few milliseconds, and if your countdown needs to show the remaining seconds, you’ll get a jump/miss (for example, starting with a “4” — missing a “5” — or jumping from a “5” to a “3” — missing a “4”).
      • Problem 3. The last onTick() to onFinish() interval is usually more than 1 second, about 2 seconds. If your countdown is counting seconds, it will be obvious that the last second is a long pause.
      • Fault 4. If the onTick time exceeds 1000 milliseconds, the onTick ticks
    • The solution
      • Look specifically at the CountDownTimer class in lib. And we’ll see that next
      • Note: In the onTick method, it is recommended to use the handler message mechanism to handle operations that take longer than one second to avoid other problems.
  • The third uses Timer to realize the Timer
    • Pay attention to the point
      • Both Timer and TimerTask have cancel methods and are best called at the same time; If cancel already, a new Timer must be created next time to schedule.
    • Possible problems
      • If you schedule a task in your current activity and press the Back button to finish the task before it finishes, the current activity’s Timer and TimerTask will not automatically cancel or destroy, it will still run in the background. If you invoke a control (such as an AlertDialog) during the task phase and the control depends on the activity being destroyed, crash will occur.
      • Therefore, it is recommended that both Timer and TimerTask terminate with cancel and set to NULL during page destruction
      • Timer mode to achieve the timing task, used to do the countdown is no problem. However, if it is used to execute periodic tasks, and there are multiple tasks, and the interval between the two tasks is shorter than the execution time of the previous task, the phenomenon of timing inaccuracy will occur. If the task runs abnormally during the Timer execution, the Timer will stop all tasks. The Timer executes periodic tasks based on the system time. The change of the system time causes the change of the Timer task execution.

03. CountDownTimer interpretation

Let’s look at a problem

  • Take a look at the case code, as shown below
    • Expected effect: ‘5-4-3-2-1-finish’ or ‘5-4-3-2-1-0’. In this case, 0 should be displayed at the same time as finish, so placing 0 in onFinish() is ok.
    mCountDownTimer = new CountDownTimer(5000, 1000) { @Override public void onTick(long millisUntilFinished) { Log.i(TAG, "---- countdown ----onTick--"+millisUntilFinished); {} public void onFinish () Log i. (TAG, "- countdown - onFinish"); }};Copy the code
  • Then look at the print log, as shown below
    The 2020-08-05 10:04:28. 742, 17266-17266 / com. Yc. Yctimer I/CountDownTimer: ---- countdown ----onTick--5000 2020-08-05 10:04:29.744 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--3998 2020-08-05 10:04:30.746 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--2997 2020-08-05 10:04:31.746 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--1996 2020-08-05 10:04:32.747 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--995 2020-08-05 10:04:33.747 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onFinish 2020-08-05 10:04:45.397 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--4999 2020-08-05 10:04:46.398 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--3998 2020-08-05 10:04:47.400 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--2996 2020-08-05 10:04:48.402 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--1994 2020-08-05 10:04:49.405 17266-17266/com.yc.yctimer I/CountDownTimer: ---- countdown ----onTick--992 2020-08-05 10:04:50.401 17266-17266/com.yc.yctimer I/CountDownTimer: ---- Countdown ----onFinishCopy the code
  • You can see that there are several problems:
    • Problem 1. Each onTick() error is a few milliseconds, not the expected “5000, 4000, 3000, 2000, 1000, 0”.
    • Problem 2. If you run it a few more times, you’ll find that the remaining seconds are off by a few milliseconds, and if your countdown needs to show the remaining seconds, you’ll get a jump/miss (for example, starting with a “4” — missing a “5” — or jumping from a “5” to a “3” — missing a “4”).
    • Problem 3. The last onTick() to onFinish() interval is usually more than 1 second, about 2 seconds. If your countdown is counting seconds, it will be obvious that the last second is a long pause.

03.3 Analysis of time error

  • Why is this a problem
    • Look at the start () method, calculation of mStopTimeInFuture (future stop the countdown time, at the end of the countdown time) with a SystemClock elapsedRealtime (), system since boot, including sleep), the number of milliseconds, It can also be called “system timestamp”.
    • [^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105 ^ # 105]. Print the log in the code below
    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 → mzippex = "+ mzippex + ", seconds =" + mzippex + +); 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; } / / remaining milliseconds final long millisLeft = mStopTimeInFuture - SystemClock. ElapsedRealtime (); if (millisLeft <= 0) { mCurrentMillisLeft = 0; if (mCountDownListener ! = null) { mCountDownListener.onFinish(); Counttimetools. I (" ^ ^ left = "+ ^ ^ left); } } else if (millisLeft < mCountdownInterval) { mCurrentMillisLeft = 0; Counttimetools. I ("handleMessage → zipleft < mCountdownInterval!" ); SendMessageDelayed (obtainMessage(MSG), millisLeft); } else {/ / have extra time long lastTickStart = SystemClock. ElapsedRealtime (); Counttimetools. I ("before onTick → lastTickStart = "+ lastTickStart); Counttimetools. I ("before onTick → ^ ^ left = "+ ^ ^ left + ", seconds =" + ^ ^ left / 1000); if (mCountDownListener ! = null) { mCountDownListener.onTick(millisLeft); CountTimeTools i. (" after onTick - elapsedRealtime = "+ SystemClock. ElapsedRealtime ()); } mCurrentMillisLeft = millisLeft; / / consider user onTick takes time, handle user onTick execution time long delay = lastTickStart + mCountdownInterval - SystemClock. ElapsedRealtime (); Counttimetools. I ("after onTick → delay1 = "+ delay); // If the user's onTick method takes longer than interval, skip to the next interval. while (delay < 0){ delay += mCountdownInterval; isWhile = true; } if (isWhile){counttimetools. I ("after onTick execution timeout → delay2 = "+ delay); } sendMessageDelayed(obtainMessage(MSG), delay); }}}};Copy the code
  • Then look at the log
    The 2020-08-05 13:36:02. 475, 8742-8742 / com. Yc. Yctimer I/CountDownTimer: Start - > mMillisInFuture = 5000, 2020-08-05 seconds = 5 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: Before onTick → zippex = 40, seconds = 40 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: Before onTick → zippex = 40, seconds = 5 ^ ^ left = 40 / zippex = 40 / zippex = 40 / zippex = 40 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: Before onTick → zippex = [^ 5 ^ 5], seconds = [^ 5 ^ 5]. 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 → ^ ^ left = [^ 5], seconds = [^ 5] + [^ 5] + [^ 5] + [^ 5] + [^ 5] + [^ 5] 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 → zipleft < mCountdownInterval! Com.yc. Yctimer: onFinish → zipleft = ^ 3Copy the code
  • Analyze the log
    • The countdown is 5 seconds, and onTick() only executes 4 times. 4,3,2,1
    • Start () starts the timer, mmilliseFuture = 5000. According to the current system time stamp (elapsedRealtime0 = 122669630, system time stamp when start() countdown), the mStopTimeInFuture of the end of the countdown relative to the start of the system is calculated.
    • There is a very short time between when handleMessage() is first entered. 122669630-122669634 = 6 milliseconds.
    • HandleMessage () is an exact calculation of program execution time. Although this is the first time you enter handleMessage, you don’t use mStopTimeInFuture directly. Instead, the remaining countdown time is calculated according to the elapsedRealtime() (elapsedRealtime1) at this point in the program execution.
    • [^ # ^ # ^ #], [^ # ^ #], [^ # ^ #]. [^ # 105 ^ # 105 ^ # 105 ^ # 105]? [^ # 105 ^ # 105 ^ # 105]? [^ # 105 ^ # 105]? These are questions 1 and 2 mentioned earlier.
    • Considering the time it takes for the user’s onTick to process the execution of the user’s onTick, sendMessageDelayed(obtainMessage(MSG), delay) is sent. See delay1 = 997 in the log

03.3 onTick Time timeout

  • According to the above analysis, the user’s onTick takes time. If delay < 0, special treatment is required. What does this mean? So let’s analyze it
  • Analyze the following action of the while loop
    / / consider user onTick takes time, handle user onTick execution time long delay = lastTickStart + mCountdownInterval - SystemClock. ElapsedRealtime (); Counttimetools. I ("after onTick → delay1 = "+ delay); Delay += mCountdownInterval; delay += mCountdownInterval; delay += mCountdownInterval; } counttimetools. I ("after onTick → delay2 = "+ delay); sendMessageDelayed(obtainMessage(MSG), delay);Copy the code
    • If the onTick() execution time is too long and exceeds the mCountdownInterval, the delay calculated after onTick() execution is negative, then the next mCountdownInterval will be skipped directly. Let delay + mCountdownInterval.
  • Let me give you an example, otherwise it’s a little bit confusing here
    • Suppose onTick() is executed every 1000 milliseconds. Assuming that the remaining countdown time before the first onTick() starts relative to the startup time of the mobile phone system is 5000 ms, the execution of this onTick() operation consumes 1015 ms, which exceeds the interval of 1000 ms set by us. So the delay of the first calculation = 1000-1015 = -15 < 0, so what does a negative number mean?
    • The onTick() call takes 1015 milliseconds, and now the countdown is 5000-1015 = 3985 milliseconds. The second onTick() is expected to start at 4000 ms, but the first onTick() has not yet finished. So the second onTick() will be delayed by -15 + 1000 = 985 milliseconds.
    • So 3985 over 1000 is 3, so we’re going to go from 5 to 3; By analogy, the execution of sendMessageDelayed after 985 milliseconds of subsequent delay will lead to a jump in time. Take a look at the following example…
  • What happens when onTick() does a time-consuming operation
    • This means that the onTick() method is not executed at this stage, and if you have business logic in this stage that is related to time nodes, there may be a bug
    The 2020-08-05 13:58:00. 657, 11912-11912 / com. Yc. Yctimer I/CountDownTimer: Start - > mMillisInFuture = 5000, 2020-08-05 seconds = 5 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: After onTick execution timeout → 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: Before onTick - millisLeft = 1873, 2020-08-05 seconds = 1 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 → -112 2020-08-05 13:58:04.896 11912-11912/com.yc.yctimer I/CountDownTimer: After onTick execution timeout → delay2 = 888 2020-08-05 13:58:05.788 11912-11912/com.yc.yctimer I/CountDownTimer: OnFinish → zipleft = -130Copy the code
  • How to execute a time-consuming operation in onTick method
    • You are advised to use the Handler message mechanism to process messages to avoid other problems.

03.4 Code improvement and perfection

  • For Questions 1 and 2:
    • Problem description
      • Problem 1. Each onTick() error is a few milliseconds, not the expected “5000, 4000, 3000, 2000, 1000, 0”.
      • Problem 2. If you run it a few more times, you’ll find that the remaining seconds are off by a few milliseconds, and if your countdown needs to show the remaining seconds, you’ll get a jump/miss (for example, starting with a “4” — missing a “5” — or jumping from a “5” to a “3” — missing a “4”).
    • The solution
      • We can increase the countdown time by a little bit, usually by manually increasing the MmillisederinFuture by a few milliseconds
    • The effect
      • Here’s an extra 20 milliseconds. Run it (for example). Countdown print log: “5,4,3,2,1, finish”,

04. Interpreting Timer and TimerTask

04.1 Timer and TimerTask methods

  • The Timer core method is shown below
    // Schedule the specified task to be executed at the specified time. If the time is in the past, the task is scheduled to be executed immediately.
    void schedule(TimerTask task, long delay)
    // Schedules the specified task to repeat < I > fixed-delay 
             , starting with the specified delay. Subsequent execution takes place at approximately regular intervals at the specified period interval.
    void schedule(TimerTask task, long delay, long period)
    Copy the code
    • The first method is executed only once;
    • For example, if delay is set to 0, the task is executed immediately. If delay is set to 1000, the task is executed for the first time with a one-second delay.
  • TimerTask core method
    • TimerTask is used to inherit (or directly define and initialize anonymous classes), rewrite the run method, and define its own business logic.
    // Cancel this timer task. If a task is scheduled for one-time execution and has not yet run, or is not scheduled, it will never run.
    // If a task is scheduled to be executed repeatedly, it will never run again. (If the task is running when this call occurs, the task will run to completion, but will no longer run.)
    public boolean cancel(a) {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            returnresult; }}Copy the code
  • About the End timer
    • Both Timer and TimerTask have cancel methods and are best called at the same time; If cancel already, a new Timer must be created next time to schedule.
    public void destroyTimer(a) {
        if(mTimer ! =null) {
            mTimer.cancel();
            mTimer = null;
        }
        if(mTimerTask ! =null) {
            mTimerTask.cancel();
            mTimerTask = null; }}Copy the code
  • Possible problems
    • If you schedule a task in your current activity and press the Back button to finish the task before it finishes, the current activity’s Timer and TimerTask will not automatically cancel or destroy, it will still run in the background. If you invoke a control (such as an AlertDialog) during the task phase and the control depends on the activity being destroyed, crash will occur.
    • Therefore, it is recommended that both Timer and TimerTask terminate with cancel and set to NULL during page destruction
    • Timer mode to achieve the timing task, used to do the countdown is no problem. However, if it is used to execute periodic tasks, and there are multiple tasks, and the interval between the two tasks is shorter than the execution time of the previous task, the phenomenon of timing inaccuracy will occur. If the task runs abnormally during the Timer execution, the Timer will stop all tasks. The Timer executes periodic tasks based on the system time. The change of the system time causes the change of the Timer task execution.

04.2 Analysis of Timer principle

  • Its basic processing model is the TaskQueue model with single thread scheduling. Timer continuously receives scheduled tasks, and all tasks join the TaskQueue after receiving Timer scheduling. TimerThread continuously retrieves tasks from the TaskQueue for execution.
    • image
  • The disadvantage of this method is that if the execution time of a task is longer than the time of the next task in the TaskQueue, the real-time performance of the whole task will be affected. In order to improve real-time performance, multiple consumers can be used to consume together to improve processing efficiency and avoid the realization of such problems.

04.3 TimerTask analysis

  • The source code is shown below
    • You can see that TimerTask is an abstract class that implements the Runnable interface. It is possible to inherit the class directly and implement its run() method, which contains the corresponding state.
    public abstract class TimerTask implements Runnable {
        final Object lock = new Object();
        int state = VIRGIN;
        // Indicates that the task has not been planned (also indicates the initial state)
        static final int VIRGIN = 0;
        // Indicates that the task is being executed
        static final int SCHEDULED   = 1;
        // Indicates that the execution is complete
        static final int EXECUTED    = 2;
        // Cancel the state
        static final int CANCELLED   = 3;
        // The next time to execute the task
        long nextExecutionTime;
        // Execution interval
        long period = 0;
        // Subclasses need to implement this method, and the code to perform the task is implemented in this method
        public abstract void run(a);
        // Cancel the task
        public boolean cancel(a) {
            synchronized(lock) {
                boolean result = (state == SCHEDULED);
                state = CANCELLED;
                returnresult; }}}Copy the code

04.4 Timer source code analysis

  • The Timer is the real core. When you create a Timer object, you also create a TimerThread object. This class integrates the Thread and essentially starts a Thread.
    public class Timer {
        Create a task queue
        private final TaskQueue queue = new TaskQueue();
        // Create a Thread object and pass in the queue
        private final TimerThread thread = new TimerThread(queue);
        public Timer(a) {
            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(); }}Copy the code
  • Then take a look at the source code for the TimerThread thread, as shown below
    • First look at mainLoop() in the run method, which starts a loop thread and blocks the current thread if there are no tasks in the queue until it wakes up after adding tasks to the queue.
    • Then, the task with the smallest execution time in the queue is acquired. If the status of the task is canceled, it is removed from the queue and retrieved from the queue again.
    • Finally, we judge whether the current time is greater than or equal to the execution time of the task. If the execution time of the task is not up, the current thread will block for a period of time, and the task will be thrown back into the task queue for re-ordering. We must ensure that the execution time of the first task in the queue is minimum.
    • After executing the mainLoop() method, newTasksMayBeScheduled uled is set to false and all tasks in the queue are cleared.
    • Think about it, what does minimum task mean here? Just keep that in mind…
    class TimerThread extends Thread {
        boolean newTasksMayBeScheduled = true;
        private TaskQueue queue;
    
        TimerThread(TaskQueue queue) {
            this.queue = queue;
        }
    
        public void run(a) {
            try {
                mainLoop();
            } finally {
                synchronized(queue) {
                // Also set the state to false
                newTasksMayBeScheduled = false;
                // Clear all tasks in the queuequeue.clear(); }}private void mainLoop(a) {
            / / while loop
            while (true) {
                try {
                    TimerTask task;
                    boolean taskFired;
                    synchronized(queue) {
                        // If the queue is empty and the flag bit is true, the thread will wait until queue. Notify is executed
                        while (queue.isEmpty() && newTasksMayBeScheduled)
                            queue.wait();
                        // If the queue is empty and jumps out of the loop, the external caller may cancel the Timer
                        if (queue.isEmpty())
                            break;
                        long currentTime, executionTime;
                        // Get the least recently executed task in the queue (that is, the most recently executed task)
                        task = queue.getMin();
                        synchronized(task.lock) {
                            // If the status of the task is cancelled, remove the task from the queue and continue with the loop queue operation
                            if (task.state == TimerTask.CANCELLED) {
                                queue.removeMin();
                                continue;
                            }
                            // Get the current system time
                            currentTime = System.currentTimeMillis();
                            // Get the next target execution time
                            executionTime = task.nextExecutionTime;
                            // If the next target execution time is greater than or equal to time, it is ready to execute the task
                            if (taskFired = (executionTime<=currentTime)) {
                                // If the task interval is 0, the task is executed only once
                                if (task.period == 0) {
                                    // Change the state of the task to executed and delete the task from the queue
                                    queue.removeMin();
                                    task.state = TimerTask.EXECUTED;
                                } else {
                                    // Rearrange tasks with tasks in the queue, always keeping the time of the first task minimal
                                    queue.rescheduleMin(task.period<0? currentTime - task.period : executionTime + task.period); }}}// Let the current thread block for a while longer
                        if(! taskFired) queue.wait(executionTime - currentTime); }// Call the task's run() to execute the code
                    if (taskFired)
                        task.run();
                } catch(InterruptedException e) {
                }
            }
        }
    }
    Copy the code
  • Then take a look at the source code for the TaskQueue queue
    • As you can see, this queue is implemented using arrays, and if it exceeds 128, it is doubled. This code is not much, the comments are very detailed, there is nothing to tell…
    public class TaskQueue {
        // Create an array of 128 to hold the tasks to be performed. If the number of tasks exceeds 128, double the number
        private TimerTask[] queue = new TimerTask[128];
        // Count the number of tasks in the queue
        private int size = 0;
        // Return the number of tasks in the queue
        int size(a) {
            return size;
        }
    
        // Set the subscript to null, which is useful for memory reclamation. Note that the subscript is 1, not 0
        void clear(a) {
            for (int i=1; i<=size; i++)
                queue[i] = null;
            size = 0;
        }
    
        // Add a new element using the minimal heap operation, which is not detailed here.
        void add(TimerTask task) {
            // If the array is full of tasks, create a new array twice as large as the previous one
            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; }}}Copy the code

04.5 schedule Indicates the schedule

  • Once we have created the Timer and started the loop thread, we need to publish the task at this point. There are several ways to publish tasks.
    • schedule(TimerTask task, Date time)
      • Indicates the time when the task is executed for the first time. The interval is 0. It also indicates that the task is executed only once
    • schedule(TimerTask task, Date firstTime, long period)
      • FirstTime indicates the time at which the task is executed for the firstTime. Period indicates the interval at which the task is executed
    • schedule(TimerTask task, long delay)
      • Delay delay time to execute the task, that is, to execute the task at the current time +delay (this method only executes the task once)
  • All three of these methods execute the Sched method, and then look at this
    • sched(TimerTask task, long time, long period)
      • All of the above functions that execute tasks end up calling this method, with task indicating the task to be executed, time indicating the time at which the task is to be executed, and period indicating the interval between tasks.
    • Take a look at the source code
      private void sched(TimerTask task, long time, long period) {
          // If the interval is greater than the average maximum value of long, the value /2 is required
          if (Math.abs(period) > (Long.MAX_VALUE >> 1))
              period >>= 1;
      
          synchronized(queue) {
              // Check whether the training thread is cancelled. If it is cancelled, an exception will be thrown
              if(! thread.newTasksMayBeScheduled)throw new IllegalStateException("Timer already cancelled.");
              synchronized(task.lock) {
                  // If the status of the newly executed task is not initialized, throw an exception
                  if(task.state ! = TimerTask.VIRGIN)throw new IllegalStateException("Task already scheduled or cancelled");
                  // Assign the time for the next task
                  task.nextExecutionTime = time;
                  task.period = period;
                  // Change the task status to publish state
                  task.state = TimerTask.SCHEDULED;
              }
              // Add the task to the minimum heap queue. Note that the first element is always the smallest before adding to the queue
              queue.add(task);
              If the task is the smallest task in the queue, wake up the TimerThread to execute the task.
              if(queue.getMin() == task) queue.notify(); }}Copy the code
    • It is clear from the above code that publishing tasks is very simple, that is, adding tasks to the task queue and judging whether conditions need to wake up the training thread to execute the task. The core code is the TimerThread rotation and the queue implementation using the minimum heap to ensure that the execution time of the first task pulled out is minimized.

04.6 Analysis of existing problems

  • The Timer obtains the task to be executed from the queue through a round-seeking thread. If the execution time of the task is not up, the Timer waits for a period of time (through the wait method of the Object class to implement blocking wait) and then wakes up the task automatically.
  • But if we are careful, we find that this is a single thread. If we have multiple tasks to execute, will we not be able to cope with it? Like a programmer who has to develop multiple requirements, if everything takes a very short time, then there is no latency problem, and if one or more things take a very long time, then it will affect the later things.
  • This phenomenon as well as the problems with the Timer is the same token, if a task is very time-consuming, and the task of the task queue and more, the TimerThread is busy not to come over, it will lead to delay the problems at the back of the task, which will affect all the accurate timing task execution time.
  • So someone might want to have a TimerTask for a Timer, right? But we have to clearly understand the computer’s system resources is limited, if we have a task is to open a separate training in rotation thread execution, is actually a bit of a waste of system resources, completely not necessary, if you don’t need regular task, we need to release resources in destroying threads, if is like this repeatedly, Bad for the smoothness of our program.

05. Custom timer case

  • In order to facilitate the realization of free and flexible setting of timer, and code simplification, can adapt to a page to create more than one timer. Or used in a list, while the countdown timer support pause, resume countdown and other functions. That’s what a special envoy has to do.
    public class CountDownTimer {
    
        /** ** time, i.e. the start time, commonly known as the total countdown time */
        private long mMillisInFuture;
        /** * Boolean value indicating whether the timer is cancelled * is set to true only when cancel is called
        private boolean mCancelled = false;
        /** * The interval at which the user receives the callback, usually 1 second */
        private long mCountdownInterval;
        /** * Record the pause time */
        private long mStopTimeInFuture;
        /** * mas.what value */
        private static final int MSG = 520;
        /** ** pause when the remaining time */
        private long mCurrentMillisLeft;
        /** * Pause is set to true only when pause is called
        private boolean mPause = false;
        /** * listener */
        private TimerListener mCountDownListener;
        /** * Whether to create start */
        private boolean isStart;
    
        public CountDownTimer(a){
            isStart = true;
        }
    
        public CountDownTimer(long millisInFuture, long countdownInterval) {
            long total = millisInFuture + 20;
            this.mMillisInFuture = total;
            //this.mMillisInFuture = millisInFuture;
            this.mCountdownInterval = countdownInterval;
            isStart = true;
        }
    
        /** * start the countdown, each click, will restart */
        public synchronized final void start(a) {
            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 → mzippex =" + mMillisInFuture + ", seconds = " + mMillisInFuture / 1000 );
            CountTimeTools.i("Start → elapsedRealtime =" + elapsedRealtime + ", → mStopTimeInFuture =" + mStopTimeInFuture);
            mPause = false;
            mHandler.sendMessage(mHandler.obtainMessage(MSG));
            if(mCountDownListener! =null){ mCountDownListener.onStart(); }}/** * cancel the timer */
        public synchronized final void cancel(a) {
            if(mHandler ! =null) {
                / / pause
                mPause = false;
                mHandler.removeMessages(MSG);
                / / cancel
                mCancelled = true; }}/** * press pause, press again to continue countdown */
        public synchronized final void pause(a) {
            if(mHandler ! =null) {
                if (mCancelled) {
                    return;
                }
                if (mCurrentMillisLeft < mCountdownInterval) {
                    return;
                }
                if(! mPause) { mHandler.removeMessages(MSG); mPause =true; }}}/** * resume pause, start */
        public synchronized final  void resume(a) {
            if (mMillisInFuture <= 0 && mCountdownInterval <= 0) {
                throw new RuntimeException("you must set the millisInFuture > 0 or countdownInterval >0");
            }
            if (mCancelled) {
                return;
            }
            // The remaining time is less than that
            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;
                    }
                    // Number of milliseconds left
                    final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
                    if (millisLeft <= 0) {
                        mCurrentMillisLeft = 0;
                        if(mCountDownListener ! =null) {
                            mCountDownListener.onFinish();
                            CountTimeTools.i("OnFinish → zipleft ="+ millisLeft); }}else if (millisLeft < mCountdownInterval) {
                        mCurrentMillisLeft = 0;
                        CountTimeTools.i("HandleMessage → zipleft < mCountdownInterval!");
                        // If the remaining time is less than one interval, no notification will be given
                        sendMessageDelayed(obtainMessage(MSG), millisLeft);
                    } else {
                        // Have time to spare
                        long lastTickStart = SystemClock.elapsedRealtime();
                        CountTimeTools.i("Before onTick → lastTickStart =" + lastTickStart);
                        CountTimeTools.i("Before onTick → zipleft =" + millisLeft + ", seconds = " + millisLeft / 1000 );
                        if(mCountDownListener ! =null) {
                            mCountDownListener.onTick(millisLeft);
                            CountTimeTools.i("After onTick → elapsedRealtime =" + SystemClock.elapsedRealtime());
                        }
                        mCurrentMillisLeft = millisLeft;
                        // Consider how long the user's onTick takes to execute
                        // Print this delay, about 997 milliseconds
                        long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();
                        CountTimeTools.i("After onTick → delay1 =" + delay);
                        // Special case: if the user's onTick method takes longer than interval, skip to the next interval
                        // Note that in the onTick callback method, do not do time-consuming operations
                        boolean isWhile = false;
                        while (delay < 0){
                            delay += mCountdownInterval;
                            isWhile = true;
                        }
                        if (isWhile){
                            CountTimeTools.i("After onTick execution timeout → delay2 ="+ delay); } sendMessageDelayed(obtainMessage(MSG), delay); }}}};/** * Set the total countdown time *@paramMillisieverts */
        public void setMillisInFuture(long millisInFuture) {
            long total = millisInFuture + 20;
            this.mMillisInFuture = total;
        }
    
        /** * Sets the countdown interval value *@paramCountdownInterval Specifies the interval. The value is 1000 milliseconds */
        public void setCountdownInterval(long countdownInterval) {
            this.mCountdownInterval = countdownInterval;
        }
    
        /** * set timer listener *@param countDownListener                 listener
         */
        public void setCountDownListener(TimerListener countDownListener) {
            this.mCountDownListener = countDownListener; }}Copy the code
  • How to use
    / / mCountDownTimer. Start (); // End the destruction of McOuntdowntimer.cancel (); / / pause mCountDownTimer. Pause (); // Resume paused McOuntdowntimer.resume ();Copy the code

Code examples:Github.com/yangchong21…