There are many ways to implement the countdown on Android. The Handler sends Message delay, the Timer and TimerTask work together, and the CountDownTimer class are used. The system-wrapped CountDownTimer is one of the easiest to use.

However, there are two usage issues with CountDownTimer that we have to pay attention to: timing inaccuracies and memory leaks. Let’s analyze the source code one by one.

Inaccurate timing problem


For a simple example, use CountDownTimer to implement a 5-second View countdown display, requiring the countdown to 1s every 1 second starting from 5s:

CountDownTimer timer = new CountDownTimer(5000.1000) {@Override
    public void onTick(long millisUntilFinished) {
        mSampleTv.setText(millisUntilFinished / 1000 + "s");
    }

    @Override
    public void onFinish(a) {}}; timer.start();Copy the code

Ideally, the TextView should behave as we expect it to, starting with “5s”, then “4s”, “3s”, and then “1s”. In fact, the display starts at “4s” (the example is simple; no diagrams here).

This indicates that there is a problem with the parameter in the onTick callback method, so add a log to the method:

Log.d("onTick", millisUntilFinished + "");Copy the code

The print result is as follows:

09-26 22:09:01. 429, 22197-22197 / com. Yifeng. The sample D/onTick: Ifeng.sample D/onTick: 3984 09-26 22:09:03.432 22197-22197/com.yifeng. Sample D/onTick: 2982 09-26 22:09:04.434 22197-22197/com.yifeng. Sample D/onTick: 1981Copy the code

As you can see, the onTick method does not start counting down from the 5000 ms we set!

Start () : CountDownTimer

public synchronized final CountDownTimer start(a) {
    mCancelled = false;
    if (mMillisInFuture <= 0) {
        onFinish();
        return this;
    }
    mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
    mHandler.sendMessage(mHandler.obtainMessage(MSG));
    return this;
}Copy the code

As you can see, CountDownTimer is also implemented internally with a Handler. At the same time, the zipgex inFuture parameter of CountDownTimer is converted to the mStopTimeInFuture value. It is worth noting that this time is automatically added along with the conversion:

SystemClock.elapsedRealtime()Copy the code

The time value of this expression represents the number of milliseconds between system startup and the execution of the current program code. Let’s move on to the Handler implementation:

private Handler mHandler = new Handler() {

    @Override
    public void handleMessage(Message msg) {

        synchronized (CountDownTimer.this) {
            if (mCancelled) {
                return;
            }

            final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();

            if (millisLeft <= 0) {
                onFinish();
            } else if (millisLeft < mCountdownInterval) {
                // no tick, just delay until done
                sendMessageDelayed(obtainMessage(MSG), millisLeft);
            } else {
                long lastTickStart = SystemClock.elapsedRealtime();
                onTick(millisLeft);

                // take into account user's onTick taking time to execute
                long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();

                // special case: user's onTick took more than interval to
                // complete, skip to next interval
                while (delay < 0) delay += mCountdownInterval; sendMessageDelayed(obtainMessage(MSG), delay); }}}};Copy the code

As you can see, where the remaining time of the countdown is calculated, the time value of the current code execution is used again:

final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();Copy the code

That is, CountDownTimer’s internal implementation is much more accurate than we’d like it to be, Take into account the very short elapsed time of the code execution between the start() method and the handleMessage() method (the main concern here is Message queue queue time).

This explains the problem we encountered earlier. The parameters for the first callback of onTick method are not set according to the countdown time we set, so the non-whole second countdown shown in the Log will appear.

Once you know why, the solution is easy. When the CountDownTimer is initialized, extend the total countdown time by an additional 0.5 seconds, or 500 milliseconds:

CountDownTimer timer = new CountDownTimer(5000 + 500.1000) {// omit the relevant code
};Copy the code

Note: one might ask, why 500 ms and not 501, 600 ms? Of course you can. The execution time from the start() call to the onTick() callback, which is essentially a piece of code, is extremely short. As you can see from the previous log, that run took only 15 milliseconds (each time the code is run, it takes a little bit more time depending on the actual usage of the Message queue that the Handler is associated with). So, in this example, anything with a decent increment of less than 1000 milliseconds is theoretically okay, as long as it’s not too small.

As an alternative, you can evaluate the zipuntilFinished conversion float and convert it to int using the rounding up mode provided by BigDecimal. It’s just a little bit more complicated.

Memory leak problem


As mentioned above, CountDownTimer is implemented internally by using the Handler mechanism, which naturally causes memory leaks.

If the CountDownTimer is not finished counting down when the Activity is closed, it keeps running in the background, holding a reference to the Activity, causing unnecessary memory leaks and even null exception errors during callback processing.

So, the way we used it is not very reasonable. We should define CountDownTimer as a global variable and then cancel the countdown when the Activity is destroyed:

@Override
protected void onDestroy(a) {
    super.onDestroy();
    if(timer ! =null) { timer.cancel(); }}Copy the code

The internal source code for the cancel() method is as follows:

public synchronized final void cancel(a) {
    mCancelled = true;
    mHandler.removeMessages(MSG);
}Copy the code

This removes the delayed Message object from the Message queue associated with the Handler.

About me: Yifeng, blog address: Yifeng. Studio /, Sina Weibo: IT Yifeng

Scan the QR code on wechat, welcome to follow my personal public account: Android Notexia

Not only share my original technical articles, but also the programmer’s workplace reverie