Handler
outline
Handler is used for message passing between threads. It can switch tasks in one thread to another thread for execution. The target thread of the switch is consistent with the thread of the Looper held inside the handler. If the Looper is not set manually when initializing the handler, the handler will obtain and hold the Looper of the current thread (when initializing the handler) through ThreadLocal. When the handler sends a message, the message will enter the MessageQueue of the target thread. The Looper of the target thread scans and takes out the message. Finally, the handler executes the message.
constructor
The constructor of Handler can be roughly divided into the following two types:
public Handler(Callback callback, boolean async){} public Handler(Looper looper, Callback callback, boolean async){} Copy code
Parameter list of constructor:
- Callback: the callback interface of the Handler to process the message, which may be called when executing the message.
- async: false by default. If the value is true, all messages in the message queue are AsyncMessage. Please refer to the following chapters for the concept of AsyncMessage.
- looper: the inquirer of the message will continuously poll to check whether there are messages in the MessageQueue.
If the caller passes a Looper, the Looper is used directly; Otherwise, the Looper is obtained from the current thread through ThreadLocal. Therefore, the target thread where the task is executed is not the thread where the Handler is created, but the thread where the Looper is located.
sendMessageAtTime
Whether you send a Message using post(Runnable r) or sendMessage(Message m), it will eventually be executed to the sendMessageAtTime method. This method specifies the executor of the Message (msg.target=handler) and the call time (msg.when).
dispatchMessage
The dispatchMessage method is used to execute pre registered Message and Handler callbacks. The source code is as follows:
public void dispatchMessage(Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } Copy code
It can be found that the priority of callback is: Message callback > handler callback (callback in constructor chapter) > handleMessage method overridden by handler subclass.
ThreadLocal
ThreadLocal is a data storage class within a thread, which is used to store data within the scope of a thread. Different data copies can be held in different threads. The Looper of the current thread can be easily found through ThreadLocal. The UML class diagram of the internal implementation of ThreadLocal is as follows:
The process of finding loopers through ThreadLocal is as follows:
- Get the current thread object through Thread.currentThread().
- Take out the ThreadLocalMap object held by the thread object.
- Take itself as the key to obtain the value of the corresponding Entry in ThreadLocalMap.
Looper
Looper plays the role of message loop in Handler. It will constantly query whether there are messages in the MessageQueue. Looper will always block when there is no message.
If the current thread does not have a Looper and the caller does not pass a Looper, the Handler will report an error because the Looper is not obtained. The solution is to manually create a loop in the current thread through loop.prepare, and start the message loop through loop.loop:
new Thread("Thread#2") { @override public void run() { Looper.prepare(); Handler handler = new Handler(); Looper.loop(); } } Copy code
Looper provides two ways to exit a looper: quit and quitsafe. The difference is that the former will exit directly; The latter exits safely after processing the existing messages of the message queue.
The thread where Looper is located will always be running, so it is recommended to exit Looper in time after message processing and release the thread.
MessageQueue
MessageQueue is the storage queue of messages, which provides many wonderful mechanisms internally.
IdleHandler
IdleHandler is essentially an abstract callback interface and does not do any operations:
/** * 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(); } Copy code
From the above comments, we can see that MessageQueue will execute the queueIdle method of IdleHandler when it is about to enter blocking. The trigger time of queue blocking is:
- Message queue has no messages.
- The execution time of the queue head message is greater than the current time.
When we want a task to be called when the queue is about to block next time, we can use IdleHandler. The most common example in Android projects is to provide callbacks outside the life cycle to activities.
For example, I want to perform an operation after the layout drawing is completed, but the onStart and onResume callbacks of the Activity are performed before the View drawing is completed. You can see the official comments of onResume:
/** * ... * <p>Keep in mind that onResume is not the best indicator that your activity * is visible to the user; a system window such as the keyguard may be in * front. Use {@link #onWindowFocusChanged} to know for certain that your * activity is visible to the user (for example, to resume a game). * ... */ @CallSuper protected void onResume() {...} Copy code
In this case, you can set an IdleHandler for the MessageQueue. When the messages in the current queue (including drawing tasks) are finished and are about to enter the blocking state, call the IdleHandler task to ensure that the task is executed after drawing.
Use as follows:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Looper.myLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { // do something when queue is idle // The return value indicates the bKeepAlive ID: true - > continue to use, false - > destroy the Handler return false; } }); } Copy code
AsyncMessage and SyncBarrier
As the name suggests, SyncBarrier represents a synchronization fence (also known as barrier message), which is used to block SyncMessage and give priority to AsyncMessage. This mechanism greatly improves the operation flexibility of MessageQueue.
Before further understanding these two concepts, you need to understand the mechanism of inserting messages into MessageQueue. The enqueueMessage source code of MessageQueue is as follows (the relevant codes of wake-up queue are omitted):
boolean enqueueMessage(Message msg, long when) { synchronized (this) { msg.markInUse(); msg.when = when; Message p = mMessages; if (p == null || when == 0 || when < p.when) { // New head. msg.next = p; mMessages = msg; } else { // Inserted within the middle of the queue. Message prev; for (;;) { prev = p; p = p.next; if (p == null || when < p.when) { break; } } msg.next = p; // invariant: p == prev.next prev.next = msg; } return true; } Copy code
It can be seen from the above source code that the messages are arranged in order according to the call time (when). When when when is equal to 0, the messages are directly inserted at the head of the queue; When when is equal to the when of the messages in the queue, the messages are inserted after these messages.
Suppose this scenario: we have a very urgent task and want to give priority to it. How should we deal with it?
Very simply, send a message with when = 0, which will be automatically inserted into the head of the list. The Handler also provides ready-made interfaces:
public final boolean postAtFrontOfQueue(Runnable r) { return sendMessageAtFrontOfQueue(getPostMessage(r)); } public final boolean sendMessageAtFrontOfQueue(Message msg) { return enqueueMessage(queue, msg, 0); } Copy code
Upgrade the scenario: we have a task a, and all other tasks depend on A. if a is not executed, all other tasks are not allowed to be executed.
A the time of inserting into the queue and the execution time are uncertain. Before that, all tasks are not allowed to be executed. This requirement cannot be realized according to the current mechanism. At this time, SyncBarrier and AsyncMessage come in handy. The implementation process is as follows:
- Call MessageQueue.postSyncBarrier to insert SyncBarrier into the queue: SyncBarrier is essentially a message with an empty target. The insertion logic is consistent with that of ordinary messages, and the insertion location is determined according to when. The when of SyncBarrier is SystemClock.uptimeMillis(), so it is inserted into the middle of the queue (there may be some messages without delay in front of SyncBarrier and messages with delay behind SyncBarrier).
- After inserting the SyncBarrier, poll the message until the SyncBarrier is queued to the queue header node. At this time, querying the message using the next method will automatically filter the synchronous message and only execute the asynchronous message. The source code is as follows:
// mMessages indicates the first message of the queue Message msg = mMessages; if (msg != null && msg.target == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } Copy code
- Insert task A (define A as AsyncMessage). Due to the existence of SyncBarrier, A will be executed preferentially (it is not excluded that A has delay, and the queue will enter the blocking state at this time, even if there may be synchronization messages without delay in the queue).
- As long as the SyncBarrier is placed at the head of the queue, the synchronization message will always be blocked, and the message queue can only output AsyncMessage. After task A is completed, you need to call removeSyncBarrier to manually remove the SyncBarrier.
Handler provides an interface for us to insert AsyncMessage, that is, the asyc parameter in the constructor. When async is true, all messages delivered through the handler will be defined as AsyncMessage (provided that it is used with SyncBarrier, otherwise AsyncMessage has no effect).
Rethinking the application scenarios of SyncBarrier and AsyncMessage mechanism is essentially to block the execution of synchronous messages from Barrier messages to AsyncMessage messages.
In the Android source code, this mechanism is used for layout drawing. In the scheduleTraversals method of ViewRootImpl, the Barrier will be set to the message queue of the main thread in advance, and then AsyncMessage will be submitted to block all synchronous messages during this period. The source code is as follows:
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; // Set up Barrier mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // This method will eventually submit an AsyncMessage mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } } Copy code
Tips: the concept of Barrier is often involved in Java concurrency, such as CountDownLatch, CyclicBarrier, etc. Please refer to chapter 21.7 of Thinking in Java for details.
Blocking and wake-up mechanism
Blocking and wake-up mechanism is the essence of MessageQueue, which greatly reduces the frequency of Loop polling and performance overhead.
The timing of message queue blocking has been mentioned in the IdleHandler section:
- Message queue has no messages.
- The execution time of the queue head message is greater than the current time.
The source code of the next method is as follows:
Message next() { int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } // The key method is to block the thread for nextPollTimeoutMillis milliseconds. If nextPollTimeoutMillis is - 1, the thread will always be blocked. nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Ignore SyncBarrier code final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages; if (msg != null) { if (now < msg.when) { // Next message is not ready. Set a timeout to wake up when it is ready. nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // Got a message. mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; msg.markInUse(); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; } // Ignore IdleHandler code if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } } } } Copy code
When to wake up the MessageQueue when inserting a message (assuming the queue is blocked):
- Insert a SyncMessage at the head of the queue.
- The head of the team is a fence, and an AsyncMessage nearest to the fence is inserted.
The source code of enqueueMessage method is as follows:
boolean enqueueMessage(Message msg, long when) { synchronized (this) { msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; needWake = mBlocked; } else { // Inserted within the middle of the queue. Usually we don't have to wake // up the event queue unless there is a barrier at the head of the queue // and the message is the earliest asynchronous message in the queue. needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; for (;;) { prev = p; p = p.next; if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { needWake = false; } } msg.next = p; // invariant: p == prev.next prev.next = msg; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { // Key method to wake up the queue thread nativeWake(mPtr); } } return true; } Copy code
The second time to wake up specifically emphasizes inserting the AsyncMessage closest to the Barrier. For the following blocking conditions, you do not need to wake up AsyncMessage when inserting it:
[image upload failed... (image-d6b8ad-1636620493886)]
Handler memory leak analysis
After understanding the internal principle of Handler, analyze the memory leakage caused by Handler:
- When a non static Handler inner class is defined, the inner class will implicitly hold the reference of the outer class.
- When the Handler executes the sendMessageAtTime method, the target parameter of the Message holds the Handler object.
- When the Message is not executed (such as now < when), if the Activity exits, the Message still holds the Handler object, while the Handler holds the Activity object, resulting in memory leakage.
Solution:
- Define Handler as a static inner class.
- Clear the corresponding Message in the MessageQueue when exiting the Activity.
Relevant video recommendations:
[Android source code] source code analysis of the whole process running handler
This article is transferred from https://juejin.cn/post/6947962229291647007 , in case of infringement, please contact to delete.