Use and principle of various messages in 10000 word double disk Handler

Keywords: Java Android Handler

We often use the Handler's send or post to schedule a delayed, non delayed or queue jumping Message. However, there are few detailed studies on when and why this Message is executed.

This article will check one by one and start the principle!

At the same time, for asynchronous Message and IdleHandler that we are not familiar with, we will demonstrate and popularize the principle. The space is large and enjoy it slowly.

Non delayed execution Message

First, create a Handler in the main thread and copy the Callback processing.

    private val mainHandler = Handler(Looper.getMainLooper()) { msg ->
        Log.d(
            "MainActivity",
            "Main thread message occurred & what:${msg.what}"
        )
        true
    }

Continuously send the Message and Runnable expected to be executed immediately to the Handler of the main thread.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        testSendNoDelayedMessages()
    }

    private fun testSendNoDelayedMessages() {
        Log.d("MainActivity","testSendNoDelayedMessages() start")
        testSendMessages()
        testPostRunnable()
        Log.d("MainActivity","testSendNoDelayedMessages() end ")
    }

    private fun testSendMessages() {
        Log.d("MainActivity","startSendMessage() start")
        for (i in 1..10) {
            sendMessageRightNow(mainHandler, i)
        }
        Log.d("MainActivity","startSendMessage() end ")
    }

    private fun testPostRunnable() {
        Log.d("MainActivity","testPostRunnable() start")
        for (i in 11..20) {
            mainHandler.post { Log.d("MainActivity", "testPostRunnable() run & i:${i}") }
        }
        Log.d("MainActivity","testPostRunnable() end ")
    }

When will it be implemented?

Before publishing the log, you can guess whether Message or Runnable will be executed immediately after send or post. If not, when will it be implemented?

 D MainActivity: testSendNoDelayedMessages() start
 D MainActivity: startSendMessage() start
 D MainActivity: startSendMessage() end
 D MainActivity: testPostRunnable() start
 D MainActivity: testPostRunnable() end
 D MainActivity: testSendNoDelayedMessages() end
 D MainActivity: Main thread message occurred & what:1
 ...
 D MainActivity: Main thread message occurred & what:10
 D MainActivity: testPostRunnable() run & i:11
 ...
 D MainActivity: testPostRunnable() run & i:20

The answer may be slightly different from that expected. A single thought seems reasonable: the sent Message or Runnable will not be executed immediately. The wake-up and callback of MessageQueue can only be executed after other work of the main thread is completed.

Why?

The non delayed sendMessage() and post() still call sendMessageAtTime() to put the Message into the MessageQueue. However, its expected execution time when becomes SystemClock.uptimeMillis(), that is, the time of the call.

// Handler.java
    public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }
    
    public final boolean post(@NonNull Runnable r) {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); // when is equal to the current time
    }

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        return enqueueMessage(queue, msg, uptimeMillis);
    }

These messages will be queued into the MessageQueue in the order of when. When the Message meets the conditions, wake will be called immediately. On the contrary, it is just inserted into the queue. Therefore, the above send or post loop will enter the queue one by one according to the call sequence, and the first Message will trigger wake.

// MessageQueue.java
    boolean enqueueMessage(Message msg, long when) {
        ...
        // In view of the situation that multiple threads send messages to the Handler
        // A lock is required before inserting a Message into the queue
        synchronized (this) {
            ...
            msg.markInUse(); // Message tag in use
            msg.when = when; // Update when attribute
            Message p = mMessages; // Get the Head of the queue 
            boolean needWake;
            // If the queue is empty
            // Or Message needs to jump the queue (sendMessageAtFrontOfQueue)
            // Or the execution time of Message is earlier than that of Head
            // The Message is inserted into the head of the queue
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                // Is the thread block ing or wait ing because there are no executable messages
                // Yes, wake up
                needWake = mBlocked;
            } else {
                // If there is a Message in the queue, the Message priority is not high, and the execution time is not earlier than the Message at the head of the queue
                // If the thread is block ing or wait ing, or a synchronization barrier is established (the target is empty), and the Message is asynchronous, it wakes up
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                // Traverse the queue to find the Message target insertion location
                for (;;) {
                    prev = p;
                    p = p.next;
                    // If the end of the queue has been traversed, or the time of the Message is earlier than the current Message
                    // Location found, exit traversal
                    if (p == null || when < p.when) {
                        break;
                    }
                    
                    // If it is decided to wake up, but the queue has an asynchronous Message with earlier execution time, do not wake up first
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                // Insert the Message into the destination of the queue
                msg.next = p;
                prev.next = msg;
            }

            // If you need to wake up, wake up the MessageQueue on the Native side
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

In summary:

  • The Message sent for the first time notifies the Native side of wake after enqueue enters the queue head of MessageQueue
  • Other messages or Runnable sent subsequently are enqueue d one by one
  • Then perform other tasks of the main thread, such as log printing
  • After idle, wake is completed, and in the next loop of next(), the queue leader Message is removed and returned to Looper for callback and execution
  • After that, loop() starts to read the next cycle of the first Message in the current queue of MessageQueue. The current time must be later than when set at send, so the messages in the queue go out of the queue and callback one by one

conclusion

The non delayed Message is not executed immediately, but is put into the MessageQueue for scheduling. The execution time is uncertain.

MessageQueue will record the time of the request and queue according to the order of time. If a lot of messages are accumulated in the MessageQueue or the main thread is occupied, the execution of the Message will be significantly later than the time of the request.

Delayed execution Message

Message with delayed execution is more common. When is it executed?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        testSendDelayedMessages()
    }

    private fun testSendDelayedMessages() {
        Log.d("MainActivity","testSendDelayedMessages() start")
        // Send Message of 2500 MS Delay
        sendDelayedMessage(mainHandler, 1)
        Log.d("MainActivity","testSendDelayedMessages() end ")
    }

The Message was sent at 28:58.186 and executed at 29:00.690. The time difference was 2504ms, which was not the exact 2500ms.

09-22 22:28:57.964 24980 24980 D MainActivity: onCreate()
09-22 22:28:58.186 24980 24980 D MainActivity: testSendDelayedMessages() start
// Send Message
09-22 22:28:58.186 24980 24980 D MainActivity: testSendDelayedMessages() end
// Message execution
09-22 22:29:00.690 24980 24980 D MainActivity: Main thread message occurred & what:1

What happens if 10 messages with an average delay of 2500ms are sent continuously?

    private fun testSendDelayedMessages() {
        Log.d("MainActivity","testSendDelayedMessages() start")
        // Continuously send 10 messages with a delay of 2500 Ms
        for (i in 1..10) {
            sendDelayedMessage(mainHandler, i)
        }
        Log.d("MainActivity","testSendDelayedMessages() end ")
    }

The execution time difference of the first Message is 2505ms (39:56.841 - 39:54.336), and the execution time difference of the tenth Message has reached 2508ms (39:56.844 - 39:54.336).

09-22 22:39:54.116 25104 25104 D MainActivity: onCreate()
09-22 22:39:54.336 25104 25104 D MainActivity: testSendDelayedMessages() start
09-22 22:39:54.337 25104 25104 D MainActivity: testSendDelayedMessages() end
09-22 22:39:56.841 25104 25104 D MainActivity: Main thread message occurred & what:1
09-22 22:39:56.842 25104 25104 D MainActivity: Main thread message occurred & what:2
09-22 22:39:56.842 25104 25104 D MainActivity: Main thread message occurred & what:3
..
09-22 22:39:56.842 25104 25104 D MainActivity: Main thread message occurred & what:8
09-22 22:39:56.842 25104 25104 D MainActivity: Main thread message occurred & what:9
09-22 22:39:56.844 25104 25104 D MainActivity: Main thread message occurred & what:10

Why?

The Delay Message execution time when is the accumulation of the sending time and the Delay time, and is queued into the MessageQueue based on this.

// Handler.java
    public final boolean postDelayed(Runnable r, int what, long delayMillis) {
        return sendMessageDelayed(getPostMessage(r).setWhat(what), delayMillis);
    }

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {  
    	return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); 
    }

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        return enqueueMessage(queue, msg, uptimeMillis); 
    }

When the Delay Message has not arrived yet, MessageQueue#next() will take the difference between the time of reading the queue and when as the duration of the next notification of Native hibernation. Before the next cycle, there are other logic in next (), which leads to the delay of wake up time. In addition, there are other tasks in the wake up thread, resulting in more delay in execution.

// MessageQueue.java
	Message next() {
        ...
        for (;;) {
            ...
            nativePollOnce(ptr, nextPollTimeoutMillis);
            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...
                
                if (msg != null) {
                    // Calculate how long the next cycle should sleep
                    if (now < msg.when) {
                        nextPollTimeoutMillis
                            = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        ...
                    }
                } else {
                    ...
                }
                ...
            }
            ...
        }
    }

conclusion

Because the calculation error of wake-up time and the callback task may occupy threads, the execution of Message is delayed. The execution time of Message is bound to be later than the time of Delay.

Jump in line to execute Message

The Handler also provides API s for Message queue jumping: sendMessageAtFrontOfQueue() and postAtFrontOfQueue().

After the above send and post, call the xxxFrontOfQueue method at the same time. What will be the execution result of Message?

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        testSendNoDelayedMessages()
        testFrontMessages() // Immediately call the FrontOfQueue method
    }

Call the API of sendMessageAtFrontOfQueue() and postAtFrontOfQueue() respectively.

    private fun testFrontMessages() {
        Log.d("MainActivity","testFrontMessages() start")
        testSendFrontMessages()
        testPostFrontRunnable()
        Log.d("MainActivity","testFrontMessages() end ")
    }

    private fun testSendFrontMessages() {
        Log.d("MainActivity","testSendFrontMessages() start")
        for (i in 21..30) {
            sendMessageFront(mainHandler, i)
        }
        Log.d("MainActivity","testSendFrontMessages() end ")
    }

    private fun testPostFrontRunnable() {
        Log.d("MainActivity","testPostFrontRunnable() start")
        for (i in 31..40) {
            mainHandler.postAtFrontOfQueue() { Log.d("MainActivity", "testPostFrontRunnable() run & i:${i}") }
        }
        Log.d("MainActivity","testPostFrontRunnable() end ")
    }

When the print logs of the main thread are output in sequence, the Message starts to execute one by one. As expected, the Message of FrontOfQueue will be executed first, that is, the earliest callback that calls the API for the last time.

After the reverse order execution of Front messages, ordinary messages are executed in the requested order.

 D MainActivity: testSendNoDelayedMessages() start
 D MainActivity: startSendMessage() start
 D MainActivity: startSendMessage() end
 D MainActivity: testPostRunnable() start
 D MainActivity: testPostRunnable() end
 D MainActivity: testSendNoDelayedMessages() end
 D MainActivity: testFrontMessages() start
 D MainActivity: testSendFrontMessages() start
 D MainActivity: testSendFrontMessages() end
 D MainActivity: testPostFrontRunnable() start
 D MainActivity: testPostFrontRunnable() end
 D MainActivity: testFrontMessages() end
 D MainActivity: testPostFrontRunnable() run & i:40
 ...
 D MainActivity: testPostFrontRunnable() run & i:31
 D MainActivity: Main thread message occurred & what:30
 ...
 D MainActivity: Main thread message occurred & what:21
 D MainActivity: Main thread message occurred & what:1
 ...
 D MainActivity: Main thread message occurred & what:10
 D MainActivity: testPostRunnable() run & i:11
 ... D MainActivity: testPostRunnable() run & i:20

How?

The principle is that the when attribute of the recorded message sent by sendMessageAtFrontOfQueue() or postAtFrontOfQueue() is fixed to 0.

// Handler.java
    public final boolean sendMessageAtFrontOfQueue(@NonNull Message msg) {
        return enqueueMessage(queue, msg, 0); // when sent is equal to 0
    }

    public final boolean postAtFrontOfQueue(@NonNull Runnable r) {
        return sendMessageAtFrontOfQueue(getPostMessage(r));
    }

As can be seen from the queue function, a Message with when = 0 will be immediately inserted into the head of the queue, so it will always be executed first.

// MessageQueue.java
    enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            ...
            // If the Message needs to jump the queue (sendMessageAtFrontOfQueue)
            // Insert team leader
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
            } else {
                ...
            }
            ...
        }
        return true;
    }

conclusion

The API s of sendMessageAtFrontOfQueue() and postAtFrontOfQueue() preset when to 0, and then insert the Message to the head of the queue. Finally, the Message is executed first.

However, it should be noted that this will result in the delayed execution of the Message which was originally executed. It may cause sequential problems for the business logic with precedence relations, and use it prudently.

Execute Message asynchronously

The messages sent by the Handler are synchronized, which means that everyone is sorted according to the order of when, who comes first and who executes.

If you encounter a Message with high priority, you can send a queue jumping Message through FrontQueue. However, if the queue that wants to be synchronized stagnates and only executes the specified Message, that is, the Message executes asynchronously, the existing API is not enough.

In fact, Android provides a synchronization barrier mechanism to meet this requirement, but it is mainly aimed at system App or system, and App can be used through reflection.

Implemented by asynchronous Handler

In addition to the commonly used Handler constructor, Handler also provides a special constructor for creating and sending asynchronous messages. Message s or Runnable sent through this Handler are asynchronous. We call it asynchronous Handler.

// Handler.java
    @UnsupportedAppUsage
    public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

    @NonNull
    public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
        if (looper == null) throw new NullPointerException("looper must not be null");
        if (callback == null) throw new NullPointerException("callback must not be null");
        return new Handler(looper, callback, true);
    }

Let's start a Handler thread to test the use of synchronization barrier: build a normal Handler and an asynchronous Handler respectively.

    private fun startBarrierThread() {
        val handlerThread = HandlerThread("Test barrier thread")
        handlerThread.start()

        normalHandler = Handler(handlerThread.looper) { msg ->
            Log.d(...)
            true
        }

        barrierHandler = Handler.createAsync(handlerThread.looper) { msg ->
            Log.d(...)
            true
        }
    }

Start the HandlerThread and send a Message to each of the two handlers.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        startBarrierThread()
        testNormalMessage()
        testSyncBarrierByHandler()
    }
    
    private fun testNormalMessage() {
        sendMessageRightNow(normalHandler, 1)
    }

    private fun testSyncBarrierByHandler() {
        sendMessageRightNow(barrierHandler, 2)
    }

Is the Message of asynchronous Handler executed first? No, because we haven't informed MessageQueue to establish a synchronization barrier!

09-24 23:02:19.032 28113 28113 D MainActivity: onCreate()
09-24 23:02:19.150 28113 28141 D MainActivity: Normal handler message occurred & what:1
09-24 23:02:19.150 28113 28141 D MainActivity: Barrier handler message occurred & what:2

In addition to sending asynchronous messages to asynchronous handlers, synchronization barriers need to be established in advance through reflection.

Note: the establishment of synchronization barrier must be earlier than the synchronization Message to be shielded, otherwise it is invalid. The following principles will be mentioned.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        startBarrierThread()
        // Establish a synchronization barrier
        postSyncBarrier(barrierHandler.looper)
        testNormalMessage()
        testSyncBarrierByHandler()
    }

    private fun postSyncBarrier(looper: Looper) {
        Log.d(...)
        val method: Method = MessageQueue::class.java.getDeclaredMethod("postSyncBarrier")
        barrierToken = method.invoke(looper.queue) as Int
    }

In this way, you can see that asynchronous messages are executed, and synchronous messages will never be executed.

09-24 23:11:36.176 28600 28600 D MainActivity: onCreate()
09-24 23:11:36.296 28600 28600 D MainActivity: Add sync barrier
09-24 23:11:36.300 28600 28629 D MainActivity: Barrier handler message occurred & what:2

The reason is that the established synchronization barrier has not been removed and only asynchronous messages in the queue are always processed. If you want to resume the execution of synchronous Message, you can remove the synchronization barrier, and you also need reflection!

We remove the synchronization barrier after the asynchronous Handler is executed.

    private fun startBarrierThread() {
        ...
        barrierHandler = Handler.createAsync(handlerThread.looper) { msg ->
            Log.d(...)
            // Remove synchronization barrier
            removeSyncBarrier(barrierHandler.looper)
            true
        }
    }

    fun removeSyncBarrier(looper: Looper) {
        Log.d(...)
        val method = MessageQueue::class.java
            .getDeclaredMethod("removeSyncBarrier", Int::class.javaPrimitiveType)
        method.invoke(looper.queue, barrierToken)
    }

You can see that the synchronous Message is restored.

09-24 23:10:31.533 28539 28539 D MainActivity: onCreate()
09-24 23:10:31.652 28539 28568 D MainActivity: Barrier handler message occurred & what:2
09-24 23:10:31.652 28539 28568 D MainActivity: Remove sync barrier
09-24 23:10:31.653 28539 28568 D MainActivity: Normal handler message occurred & what:1

Implemented through asynchronous Message

When there is no special asynchronous Handler, you can send a message with the isAsync attribute of true to the ordinary Handler. The effect is the same as that of the asynchronous Handler. Of course, this method still needs to establish a synchronization barrier.

Add the overloaded parameter of isAsync to the original function of sending Message.

    private fun sendMessageRightNow(handler: Handler, what: Int, isAsync: Boolean = false) {
        Message.obtain().let {
            it.what = what
            it.isAsynchronous = isAsync
            handler.sendMessage(it)
        }
    }

Send an asynchronous Message to a normal Handler.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        testNormalMessage()
        // Send asynchronous messages using Message mode instead
        testSyncBarrierByMessage()
    }

    private fun testSyncBarrierByMessage() {
        sendMessageRightNow(normalHandler, 2, true)
    }

Also remember to remove the synchronization barrier after the asynchronous Message is received.

    private fun startBarrierThread() {
        ...
        normalHandler = Handler(handlerThread.looper) { msg ->
            Log.d(...)
            if (2 == msg.what) removeSyncBarrier(barrierHandler.looper)
            true
        }
    }

The result is consistent with the asynchronous Handler.

09-24 23:58:05.801 29040 29040 D MainActivity: onCreate()
09-24 23:58:05.923 29040 29040 D MainActivity: Add sync barrier
09-24 23:58:05.923 29040 29070 D MainActivity: Normal handler message occurred & what:2
09-24 23:58:05.924 29040 29070 D MainActivity: Remove sync barrier
09-24 23:58:05.924 29040 29070 D MainActivity: Normal handler message occurred & what:1

principle

Let's take a look at how the synchronization barrier is established.

// MessageQueue.java
	// The default is to establish the barrier at the time of the call
	public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

    // The synchronization barrier supports specifying the start time
	// The default is the time of call, and 0 represents "time of call"?
	private int postSyncBarrier(long when) {
        synchronized (this) {
            // Multiple synchronization barriers can be established and identified with counted Token variables
            final int token = mNextBarrierToken++;

            // Get a Message
            // Its target attribute is empty
            // Specify the when attribute as the start time of the barrier
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            // Store Token in Message
            // To identify the corresponding synchronization barrier
            msg.arg1 = token;

            // In the order of when
            // Find the appropriate place to insert the Message into the queue
            // Therefore, if the establishment of the synchronization barrier is called late
            // Then the Message before it cannot be blocked
            Message prev = null;
            Message p = mMessages;
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }

            // Insert Message into
            if (prev != null) {
                msg.next = p;
                prev.next = msg;
            } else {
                // If there is no Message in the queue
                // Or the Message moment of the team leader
                // If it's later than Message
                // Insert barrier Message into the head of the queue
                msg.next = p;
                mMessages = msg;
            }
            
            // Return the above Token to the caller
            // It is mainly used to remove the corresponding barrier
            return token;
        }
    }

Let's look at how asynchronous Message is executed.

// MessageQueue.java
	Message next() {
        ...
        for (;;) {
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                // If the head of the team is Message
                // Traverse to find the next asynchronous Message
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                // No synchronization barrier is established and there is a Message in the team
                // perhaps
                // The synchronization barrier is established and the asynchronous Message is found
                if (msg != null) {
                    // If the current time is earlier than the target execution time
                    if (now < msg.when) {
                        // Update the timeout that the next cycle should sleep
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        mBlocked = false;
                        
                        // Message found and out of the queue
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }

                        // Message return
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // There is no Message on the team
                    // Or a synchronization barrier has been established, but there is no asynchronous Message
                    // Infinite sleep
                    nextPollTimeoutMillis = -1;
                }
                ...
            }
            ...
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

Finally, let's look at how the synchronization barrier is removed.

// MessageQueue.java
	// The Token returned when passing in add is required
	public void removeSyncBarrier(int token) {
        synchronized (this) {
            Message prev = null;
            Message p = mMessages;
            // Traverse the queue until a barrier Message matching the token is found
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
            
            // If it is not found, an exception will be thrown
            if (p == null) {
                throw new IllegalStateException("The specified message queue synchronization "
                        + " barrier token has not been posted or has already been removed.");
            }
            final boolean needWake;
            
            // Remove barrier Message
            
            // If Message is not at the head of the team
            // No wake-up required
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                // Barrier Message at the head of the team
                // And the new team leader exists and is not another barrier
                // Need to wake up immediately
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            p.recycleUnchecked();

            // Wake up to process subsequent messages immediately
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }

Briefly summarize the principle:

  1. Establishment of synchronization barrier: put a barrier Message (target attribute is null) at the appropriate position according to the call time when, and get the count token identifying the barrier and store it in the barrier Message
  2. If a barrier Message is found when reading the queue, it will traverse the queue and return the earliest executed asynchronous Message
  3. Removal of synchronization barrier: find the matching barrier Message in the queue according to the token to perform the queue exit operation. If there is a Message at the head of the queue and it is not another synchronization barrier, wake up the looper thread immediately

Conclusion and Application

Conclusion:

  • You can add asynchronous messages to MessageQueue through asynchronous Handler or asynchronous Message
  • However, a synchronization barrier needs to be established in advance, and the establishment time of the barrier must be before the blocked Message is sent
  • Multiple synchronization barriers can be established, which will be queued according to the specified time and identified by counting tokens
  • Remember to remove the synchronization barrier after it is used, otherwise subsequent messages will be blocked forever

The difference between queue jumping and Message execution:

  • Queue jumping messages can only be executed first, and subsequent messages have to be executed after completion
  • Asynchronous Message is different. Once the synchronization barrier is established, it will remain dormant until the asynchronous Message arrives. Only when the synchronization barrier is revoked can subsequent messages resume execution

Application:

The most typical use of asynchronous messages in the AOSP system is screen refresh. The refreshed messages do not want to be blocked by the Message queue of the main thread, so a synchronization barrier will be established before sending the refreshed messages to ensure that the refresh tasks are executed first.

// ViewRootImpl.java
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

	final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

Send an asynchronous Message after the barrier is established.

// Choreographer.java
	private void postCallbackDelayedInternal(...) {
        synchronized (mLock) {
            ...

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

IdleHandler "Message"

The IdleHandler provided by MessageQueue allows the queue to call back the specified logic (queueIdle()) when idle. It is not a Message type in essence, but it is similar to the logic of Message when scheduling in MessageQueue. Let's understand it as a special "Message".

It's easy to use. Just call addIdleHandler() of MessageQueue to add the implementation. If you don't need to execute again after execution, you need to call removeIdleHandler() to remove it or return false in the callback.

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        testIdleHandler()
    }

    private fun testIdleHandler() {
        Log.d("MainActivity","testIdleHandler() start")
        mainHandler.looper.queue.addIdleHandler {
            Log.d("MainActivity", "testIdleHandler() queueIdle callback")
            false
        }
        Log.d("MainActivity","testIdleHandler() end ")
    }

You can see that the addIdleHandler is not executed immediately after it is called, but it takes hundreds of ms for queueIdle() to be executed.

09-23 22:56:46.130  7732  7732 D MainActivity: onCreate()
09-23 22:56:46.281  7732  7732 D MainActivity: testIdleHandler() start
09-23 22:56:46.281  7732  7732 D MainActivity: testIdleHandler() end
09-23 22:56:46.598  7732  7732 D MainActivity: testIdleHandler() queueIdle callback

If a string of non delayed messages is sent after the addIdleHandler call, does queueIdle() execute first or later?

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        testIdleHandler()
        testSendMessages()
    }

The results show that after a pile of messages are executed, it still takes hundreds of ms for queueIdle() to be executed.

09-23 23:07:50.639  7926  7926 D MainActivity: onCreate()
09-23 23:07:50.856  7926  7926 D MainActivity: testIdleHandler() start
09-23 23:07:50.856  7926  7926 D MainActivity: testIdleHandler() end
09-23 23:07:50.856  7926  7926 D MainActivity: startSendMessage() start
09-23 23:07:50.857  7926  7926 D MainActivity: startSendMessage() end
09-23 23:07:50.914  7926  7926 D MainActivity: Main thread message occurred & what:1
...
09-23 23:07:50.916  7926  7926 D MainActivity: Main thread message occurred & what:10
09-23 23:07:51.132  7926  7926 D MainActivity: testIdleHandler() queueIdle callback

It can also be understood from the above results that there are still a pile of messages waiting to be processed in the MessageQueue, which is not idle. Therefore, you need to execute it before you have the opportunity to call back queueIdle().

What if you send a delayed Message?

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    testIdleHandler()
    testSendDelayedMessages()
}

Because the Message sent is a delayed Message, the MessageQueue is temporarily idle, and the IdleHandler will be taken out for processing first.

09-23 23:21:36.135  8161  8161 D MainActivity: onCreate()
09-23 23:21:36.339  8161  8161 D MainActivity: testIdleHandler() start
09-23 23:21:36.340  8161  8161 D MainActivity: testIdleHandler() end
09-23 23:21:36.340  8161  8161 D MainActivity: testSendDelayedMessages() start
09-23 23:21:36.340  8161  8161 D MainActivity: testSendDelayedMessages() end
09-23 23:21:36.729  8161  8161 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:21:38.844  8161  8161 D MainActivity: Main thread message occurred & what:1
...
09-23 23:21:38.845  8161  8161 D MainActivity: Main thread message occurred & what:10

The above queueIdle() returns false to ensure that the Handler is removed after processing.

However, if true is returned and removeIdleHandler() is not called, the Handler will be executed when it is idle. This should be noted!

    private fun testIdleHandler() {
        mainHandler.looper.queue.addIdleHandler {
            ...
            true // false
        }
    }

queueIdle() has been called back many times because it has not been removed. This is because Looper will call back IdleHandler when it finds that there is no Message after it has not executed a Message, until there is no Message in the queue.

09-23 23:24:04.765  8226  8226 D MainActivity: onCreate()
09-23 23:24:05.010  8226  8226 D MainActivity: testIdleHandler() start
09-23 23:24:05.011  8226  8226 D MainActivity: testIdleHandler() end
09-23 23:24:05.368  8226  8226 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:24:05.370  8226  8226 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:24:05.378  8226  8226 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:24:05.381  8226  8226 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:24:05.459  8226  8226 D MainActivity: testIdleHandler() queueIdle callback

If a delayed Message is sent after add ing the IdleHandler that cannot be removed, the idle Message will be executed again.

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    testIdleHandler()
    sendDelayedMessage(mainHandler, 1)
}
09-23 23:31:53.928  8620  8620 D MainActivity: onCreate()
09-23 23:31:54.042  8620  8620 D MainActivity: testIdleHandler() start
09-23 23:31:54.042  8620  8620 D MainActivity: testIdleHandler() end
09-23 23:31:54.272  8620  8620 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:31:54.273  8620  8620 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:31:54.278  8620  8620 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:31:54.307  8620  8620 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:31:54.733  8620  8620 D MainActivity: testIdleHandler() queueIdle callback
09-23 23:31:56.546  8620  8620 D MainActivity: Main thread message occurred & what:1
09-23 23:31:56.546  8620  8620 D MainActivity: testIdleHandler() queueIdle callback

Why?

The callback of queueIdle() is called back by MessageQueue#next().

// MessageQueue.java
	Message next() {
        ...
        // The initial of the loop sets the pending IdleHandler count to - 1
        // Ensure that the existence and calling of Idle Handler can be checked for the first time
        int pendingIdleHandlerCount = -1;
        int nextPollTimeoutMillis = 0;
        for (;;) {
            ...
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                // If the Message at the head of the queue and the synchronization barrier is established, look for the next asynchronous Message
                if (msg != null && msg.target == null) {
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                
                // Found the right Message
                if (msg != null) {
                    // If the current time is earlier than the target execution time
                    // Set the sleep timeout, that is, the difference between the current time and the target time
                    if (now < msg.when) {
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        mBlocked = false;
                        
                        // When the time conditions are met, the Message is out of the queue
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }

                        // And return Message
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // There is no suitable Message on the team
                    // Enter infinite sleep
                    nextPollTimeoutMillis = -1;
                }

                // If you are exiting Looper, end the loop and return null
                // Will cause loop() to exit
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If there is no appropriate Message and Looper does not exit
                // Check whether there is an Idle Handler to process

                // Read the Idle Handler list
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                
                // If there is no Idle Handler to process for the time being, enter the next cycle
                // In order to give the next loop a chance to execute if a new Idle Handler appears
                // The counter is not reset and remains the initial value - 1
                if (pendingIdleHandlerCount <= 0) {
                    mBlocked = true;
                    continue;
                }

                // If IdleHandler exists, copy it to the pending list
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Traverse pending Idle Handlers
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null;

                boolean keep = false;
                try {
                    // Callback queueIdle() one by one
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                // If the callback returns false, it will be removed from the Idle list
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the IdleHandler count after processing
            // Ensure that IdleHandler will not be processed repeatedly in the next cycle
            pendingIdleHandlerCount = 0;
            nextPollTimeoutMillis = 0;
        }
    }

There are several details to note:

  1. The first time in the next() loop, count is set to - 1 to ensure that the IdleHandler will be processed when the queue is idle
  2. If there is no IdleHandler available for processing, go directly to the next cycle, and keep the disposal of count to ensure that the next cycle can check whether a new IdleHandler is added
  3. After the normal processing of IdleHandler, avoid repeated processing in the next cycle, and set the count to 0 to ensure that it will not be checked next time. Note: it is the next cycle, not a permanent non inspection

Conclusion and Application

Conclusion:
IdleHandler can implement the task execution in the idle state of MessageQueue, such as some lightweight initialization tasks at startup. However, since the execution time depends on the Message status of the queue, it is not controllable. Use it with caution!

Application: the IdleHandler mechanism is used in many places in the AOSP source code. For example, the ActivityThread uses it to collect GC in an idle state.

// ActivityThread.java
	final class GcIdler implements MessageQueue.IdleHandler {
        @Override
        public final boolean queueIdle() {
            doGcIfNeeded();
            purgePendingResources();
            return false;
        }
    }
	
	void scheduleGcIdler() {
        if (!mGcIdlerScheduled) {
            mGcIdlerScheduled = true;
            Looper.myQueue().addIdleHandler(mGcIdler);
        }
        mH.removeMessages(H.GC_WHEN_IDLE);
    }

    void unscheduleGcIdler() {
        if (mGcIdlerScheduled) {
            mGcIdlerScheduled = false;
            Looper.myQueue().removeIdleHandler(mGcIdler);
        }
        mH.removeMessages(H.GC_WHEN_IDLE);
    }

Term summary

Non delayed, delayed and queue jumping messages are widely used, so there is no need to repeat them. However, several other obscure Message terms need to be summarized for quick comparison and deeper understanding.

Message termsintroduce
Asynchronous MessageIf the isAsync attribute is true, the Message that needs to be executed asynchronously needs to be used with the synchronization barrier
Asynchronous HandlerHandler dedicated to sending asynchronous messages
Barrier MessageThe Message instance with empty target and holding token information is put into the queue as the starting point of the synchronization barrier
Synchronous barrierInsert a barrier Message at the specified time in the MessageQueue to ensure that only asynchronous messages are executed
Idle IdleHandlerThe processing interface used to call back when the MessageQueue is idle. If it is not removed, it will be executed every time the queue is idle

epilogue

The above has demonstrated and explained the principles of various Message and IdleHandler. I believe I have a deeper understanding of its details.

Here is a simple summary:

  • Non delayed execution of Message: it is not executed immediately, but queued and scheduled according to the time of the request, which ultimately depends on the order of the queue and whether the main thread is idle
  • Delayed execution of Message: it is not executed immediately at the Delay time. The execution time must be later than the Delay time due to wake-up error and thread task blocking
  • Jump in the queue to execute Mesage: similarly, it is not executed immediately, but the task is placed at the head of the team every time to achieve the purpose of executing first, but the execution sequence is disrupted, and there is a logical hidden danger
  • Asynchronous Message: the system is mostly used, while App needs reflection. Through this mechanism, you can jump in the queue and ensure that other messages are blocked. Learn
  • IdleHandler "Message": the system is widely used to implement the task execution when the MessageQueue is idle, but the execution time is uncontrollable. It is best to remove it after execution and use it with caution

Posted by chele on Sat, 25 Sep 2021 11:10:18 -0700