We all know that Android is based on message processing. For example, the application startup process, the Activity startup, and the user click behavior are all related to the Handler. The Handler is responsible for the message processing in Android. MessageQueue and Looper, Message is also a very important object, while IdleHandler is the static internal interface to MessageQueue.

IdleHandler, which is an IdleHandler that is executed only when there are no messages in the message queue or when the messages in the queue are not ready to run, and is stored in the mPendingIdleHandlers queue.

    /** * Callback interface for discovering when a thread is going to block * waiting for more messages. */
    public static interface IdleHandler {
        /** * Called when the message queue has run out of messages and will now * wait for more. Return true to keep your idle handler active, false * to have it removed. This may be called if there are still messages * pending in the queue, but they are all scheduled to be dispatched * after the current time. */
        boolean queueIdle(a);
    }
Copy the code

IdleHandler is a callback interface, when the message queue in the thread will block waiting for the message, will call back the interface, that is, the message queue in the message processing is finished, no new message, in the idle state will call back the interface.

Then let’s see how it is used in ordinary times:

        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle(a) {
              	// Process logic when idle
              
                return false; }});Copy the code

As you can see from the previous class definition, returning false means that the callback is removed after execution, and returning true means that the IdleHandler is always active and will be called back whenever idle. In terms of code usage, this is essentially a listening callback, so where is the trigger?

The trigger IdleHandler

To understand what triggers are, let’s take a quick look at the messaging mechanisms in Android. Android is based on message processing mechanism for event processing, user click behavior is delivered through messages. In order for an application to respond quickly to user clicks, it must first be alive (and running). Android is designed to have an infinite loop in the application, in which if there are no messages to be processed, it goes to sleep and wakes up as soon as a message arrives. To see where this loop is in Android, let’s go to the Main () method of the ActivityThread:

//ActivityThread.java
public static void main(String[] args) {
    ···
    Looper.prepareMainLooper(); / / to create stars... ActivityThread thread =new ActivityThread();
    thread.attach(false, startSeq);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop(); // The location of the dead loop

    throw new RuntimeException("Main thread loop unexpectedly exited");
}
Copy the code

Look again at the code in looper.loop ()

    public static void loop(a) {
        finalLooper me = myLooper(); ...finalMessageQueue queue = me.mQueue; ...for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return; }, MSG. RecycleUnchecked (); }}Copy the code

You can see that there is a for(;;) in the loop method. If queue.next() returns null, the entire activityThread.main () method ends, and the program exits. But our program must always be running, which means there are always messages in queue.next(), but if there is no action for a while, there is no execution message in the whole program, so why does the program continue to run, so the problem must be in queue.next().

This method also has a for(;;) In an endless loop, there is a key method called nativePollOnce

    @UnsupportedAppUsage
    Message next(a) {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported....int nextPollTimeoutMillis = 0;
        for (;;) {
            if(nextPollTimeoutMillis ! =0) {
                Binder.flushPendingCommands();
            }
						···
            nativePollOnce(ptr, nextPollTimeoutMillis); // No message, blocking wait...}}Copy the code

This method blocks at nextPollTimeoutMillis = -1 and waits until the next message is available. Otherwise, proceed down. So where do we wake up? Is in the method enqueueMessage, the final execution of message enqueueMessage:

boolean enqueueMessage(Message msg, long when) {...// We can assume mPtr ! = 0 because mQuitting is false.
            if(needWake) { nativeWake(mPtr); }}return true;
    }
Copy the code

Wake up in the nativeWake method, the one above, which is blocked when there is no message.

The IdleHandler is used when there is no message in the message queue or when there is a message that does not need to be processed temporarily (delayed message), that is, this time is idle for IdleHandler processing. So we can assume that the IdleHandler should also do the method that triggers it in the next method. And so it is:

Message next(a) {...for(;;) {...synchronized (this) {
        // This is normal message queue processing.if (mQuitting) {
                    dispose();
                    return null;
                }
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run. Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
              	//mIdleHandlers array, assigned to mPendingIdleHandlers
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
                if(! keep) {synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0; }}Copy the code

MessageQueue has two declarations about IdleHandler:

  • ArrayList(mIdleHandlers) to store idleHandlers,
  • There is also an IdleHandler array (mPendingIdleHandlers).

Handlers[I] = null (mPendingIdleHandlers[I] = null); In the next() method of MessageQueue

The general process is like this:

  1. If the loop receives an empty Message, or if the Message is delayed and has not reached the specified trigger time, the current queue is considered idle.
  2. We then iterate through the mPendingIdleHandlers array (the elements of which are picked up in mIdleHandlers each time) to call the queueIdle method of each IdleHandler instance.
  3. If this method returns false, the instance is removed from mIdleHandlers, meaning that it does not continue to call its queueIdle method the next time the queue is idle.

After processing the IdleHandler, nextPollTimeoutMillis is set to 0, which does not block the message queue. It is important to note that this code is also not too time-consuming, since it is executed synchronously and will definitely affect subsequent message execution.

System source code for use

Know how the IdleHandler triggered, we’ll look at how to use it when the system source code, such as ActivityThread. Idle in ActivityThread. HandleResumeActivity () call.

 @Override
3808    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
3809            String reason) {
3810        // If we are getting ready to gc after going to the background, well
3811        // we are back active so skip it.
3812        unscheduleGcIdler();
3813        mSomeActivitiesChanged = true; ...// This method eventually executes the onResume method
3816        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
3817        if (r == null) {
3818            // We didn't actually resume the activity, so skipping any follow-up actions.
3819            return;
3820} · · · · · ·3921
3922        r.nextIdle = mNewActivities;
3923        mNewActivities = r;
3924        if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
3925        Looper.myQueue().addIdleHandler(new Idler());
3926    }
Copy the code

In the handleResumeActivity() method, looper.myQueue ().addidleHandler (new Idler()) is executed at the end. So what is this Idler for? Let’s start with the inner class definition:

1829    private class Idler implements MessageQueue.IdleHandler {
1830        @Override
1831        public final boolean queueIdle(a) {
1832ActivityClientRecord a = mNewActivities; ...1838            if(a ! =null) {
1839                mNewActivities = null;
1840                IActivityManager am = ActivityManager.getService();
1841                ActivityClientRecord prev;
1842                do {
  											// Print some logs
1843                    if (localLOGV) Slog.v(
1844                        TAG, "Reporting idle of " + a +
1845                        " finished=" +
1846(a.activity ! =null && a.activity.mFinished));
1847                    if(a.activity ! =null && !a.activity.mFinished) {
1848                        try {
  															//AMS does some resource collection
1849                            am.activityIdle(a.token, a.createdConfig, stopProfiling);
1850                            a.createdConfig = null;
1851                        } catch (RemoteException ex) {
1852                            throw ex.rethrowFromSystemServer();
1853                        }
1854                    }
1855                    prev = a;
1856                    a = a.nextIdle;
1857                    prev.nextIdle = null;
1858                } while(a ! =null);
1859            }
1860            if (stopProfiling) {
1861                mProfiler.stopProfiling();
1862            }
								// Make sure Jit is working, otherwise throw an exception
1863            ensureJitEnabled();
1864            return false;
1865        }
1866    }
Copy the code

You can see that the queueIdle method does things like recycle, which will be explained in more detail below, but these things wait until the onResume method is done, and the screen shows that the more important things have been done, and they can be done at a free time. In other words, the design logic of the system is to ensure that the most important logic is executed first, and then to deal with other secondary things.

But if there are messages in the MessageQueue queue all the time, then IdleHandler never gets a chance to execute, then onStop, onDestory will not be executed. Not like that, in the resumeTopActivityInnerLocked () – > completeResumeLocked () – > scheduleIdleTimeoutLocked () method will send a will send a delayed message (s), If the interface has not been closed for a long time (if the interface needs to be closed), the message will be triggered 10 seconds later to close the interface and execute methods such as onStop.

Common Usage

  1. We may want to defer some of the lower priority operations when the application starts upHandler.postDelayed(Runnable r, long delayMillis)To achieve, but do not know how much delay is appropriate, because mobile phone performance is different, some poor performance may need more delay, some good performance can allow less delay time. So when doing project performance tuning, you can use IdleHandler, which executes tasks when the main thread is idle without affecting the execution of other tasks.
  2. If you want to add other views that depend on a View after it has been drawn, of course this is usefulView.post()Can also be implemented, the difference is that the former is executed when the message queue is idle
  3. Sending an IdleHandler that returns true to keep a View flashing so that when the user is in a daze they can be induced to click on the View, which is also cool

Use of third-party libraries

LeakCanary (1.5 source)

Let’s look at the use of LeakCanary, which is in the AndroidWatchExecutor class

public final class AndroidWatchExecutor implements WatchExecutor {

  static final String LEAK_CANARY_THREAD_NAME = "LeakCanary-Heap-Dump";
  private final Handler mainHandler;
  private final Handler backgroundHandler;
  private final long initialDelayMillis;
  private final long maxBackoffFactor;

  public AndroidWatchExecutor(long initialDelayMillis) {
    mainHandler = new Handler(Looper.getMainLooper());
    HandlerThread handlerThread = new HandlerThread(LEAK_CANARY_THREAD_NAME);
    handlerThread.start();
    backgroundHandler = new Handler(handlerThread.getLooper());
    this.initialDelayMillis = initialDelayMillis;
    maxBackoffFactor = Long.MAX_VALUE / initialDelayMillis;
  }
	// Initial call
  @Override public void execute(Retryable retryable) {
    // Eventually we switch to the main thread and call waitForIdle()
    if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
      waitForIdle(retryable, 0);
    } else {
      postWaitForIdle(retryable, 0); }}private void postWaitForIdle(final Retryable retryable, final int failedAttempts) {
    mainHandler.post(new Runnable() {
      @Override public void run(a) { waitForIdle(retryable, failedAttempts); }}); }/ / IdleHandler use
  void waitForIdle(final Retryable retryable, final int failedAttempts) {
    // This needs to be called from the main thread.
    Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override public boolean queueIdle(a) {
        postToBackgroundWithDelay(retryable, failedAttempts);
        return false; }}); }// The final call method
	private void postToBackgroundWithDelay(final Retryable retryable, final int failedAttempts) {
    long exponentialBackoffFactor = (long) Math.min(Math.pow(2, failedAttempts), maxBackoffFactor);
    long delayMillis = initialDelayMillis * exponentialBackoffFactor;
    backgroundHandler.postDelayed(new Runnable() {
      @Override public void run(a) {
        Retryable.Result result = retryable.run();
        if (result == RETRY) {
          postWaitForIdle(retryable, failedAttempts + 1); } } }, delayMillis); }}Copy the code

To see where execute() is called, we know that LeakCancary is refwatch.watch () from the interface destroy onDestroy method, Watch () -> ensureGoneAsync() -> execute()

private void ensureGoneAsync(final long watchStartNanoTime, final KeyedWeakReference reference) {
    watchExecutor.execute(new Retryable() {
      @Override public Retryable.Result run(a) {
        returnensureGone(reference, watchStartNanoTime); }}); }Copy the code

EnsureGone () does GC collection and some analysis, so we can see that LeakCanary does not do GC collection and some analysis after onDestry. Instead, IdleHandler is used to do this when it is idle, as little as possible to affect the main thread.

Matters needing attention

There are a few other things to note about the use of IdleHandler, as well:

  1. MessageQueue provides add/remove IdleHandler methods, but we don’t necessarily need to use them in pairs because idleHandler.queueidle () can remove the IdleHanlder if it returns false.
  2. Do not manage unimportant startup services in IdleHandler because its processing timing is not controllable. If MessageQueue is always waiting for messages to be processed, its execution time will be too late.
  3. Why not go into an infinite loop when mIdleHanders are never empty?
    • MIdleHander is only attempted if pendingIdleHandlerCount is -1;
    • PendingIdlehanderCount starts with -1 in next() and is set to 0 after a single execution, so it is not repeated;

conclusion

From the above analysis, we already know that IdleHandler is the static internal interface to MessageQueue, which is executed when the queue is idle, and how it is triggered, which is related to the message mechanism. We also learned how it is used in source code, how it is used in application development in general, how it is used in third-party frameworks, and finally, a few considerations.