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:
- 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
- If a barrier Message is found when reading the queue, it will traverse the queue and return the earliest executed asynchronous Message
- 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:
- 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
- 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
- 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 terms | introduce |
---|---|
Asynchronous Message | If the isAsync attribute is true, the Message that needs to be executed asynchronously needs to be used with the synchronization barrier |
Asynchronous Handler | Handler dedicated to sending asynchronous messages |
Barrier Message | The Message instance with empty target and holding token information is put into the queue as the starting point of the synchronization barrier |
Synchronous barrier | Insert a barrier Message at the specified time in the MessageQueue to ensure that only asynchronous messages are executed |
Idle IdleHandler | The 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