Write in front
Finished last timeBinder study notesAfter that, I will check the startup process of Activity again. Because I understand the basic principles of Binder, I will be more impressed this time and the learning effect will be much better than before. I originally planned to write the Activity startup process directly, but I always felt that Handler also needed to write it. After knowing the principles of Handler and Binder, and then looking at the Activity startup process, there should be no problem. Although there are already many Handler-related articles on the Internet, and the upper principles of the Handler mechanism are not difficult, I decided to write them because I want to build my own knowledge system. I also hope to give my friends who read my blog a seamless reading experience.
The main classes involved in the Handler mechanism include Handler, Message, Looper, MessageQueue, ThreadLocal, etc. Although we are most familiar with the two classes Handler and Message, before we can start using Handler, Looper did some things for us.
The source code of this article is based on Android-28
Looper
Before using Handler, we have to initialize the Looper and let the Looper run.
(); ... ();
After executing the above two statements, Looper can run. Let’s take a look at the corresponding source code:
public static void prepare() { prepare(true); } private static void prepare(boolean quitAllowed) { if (() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } (new Looper(quitAllowed)); } private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = (); }
It is necessary to ensure that there is and only one Looper object in a thread, so when initializing Looper, it will check whether there is a Looper object in the current thread. Looper initialization creates a MessageQueue. After creating the Looper, it will be placed in ThreadLocal. Regarding ThreadLocal, we will talk about it later.
public static void loop() { // Determine whether the current thread has initialized the Looper final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; () wasn't called on this thread."); } final MessageQueue queue = ; ... for (;;) { Message msg = (); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } ... final long traceTag = ; if (traceTag != 0 && (traceTag)) { (traceTag, (msg)); } try { // target refers to Handler (msg); } finally { if (traceTag != 0) { (traceTag); } } ... (); } }
The method is relatively long, so I only put out the most core code. There is one more interesting code omitted: we can specify a threshold value such as 200. When the processing of Message exceeds 200ms, the Log will be output. This can help us identify some potential performance issues in development. Unfortunately, the method of setting thresholds is hidden and cannot be called directly, so the code will not be released here. Interested friends can search the source code by themselves.
The simplified code shows that the logic is very simple. It can be said that Looper plays the role of a brick mover in it. He takes out the Message from the MessageQueue, then hand it over to the Handler to distribute, and then goes to the MessageQueue to retrieve the Message... endlessly, just like Yugong moving a mountain.
When you see this, you should feel something is wrong, because this is a dead loop, which will logically occupy CPU resources, and there will always be a time when the message is processed. Will you return Null from the message queue after processing and then Looper end? Obviously not, pay attention to the comment might block.
MessageQueue
The answer is in MessageQueue, let’s take a look at next():
Message next() { ... int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { if (nextPollTimeoutMillis != 0) { (); } nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = (); Message prevMsg = null; Message msg = mMessages; if (msg != null && == null) { // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = ; } while (msg != null && !()); } if (msg != null) { if (now < ) { // Next message is not ready. Set a timeout to wake up when it is ready. nextPollTimeoutMillis = (int) ( - now, Integer.MAX_VALUE); } else { // Got a message. mBlocked = false; if (prevMsg != null) { = ; } else { mMessages = ; } = null; if (DEBUG) (TAG, "Returning message: " + msg); (); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; } // Process the quit message now that all pending messages have been handled. if (mQuitting) { dispose(); return null; } // If first time idle, then get the number of idlers to run. // Idle handles only run if the queue is empty or if the first message // in the queue (possibly a barrier) is due to be handled in the future. if (pendingIdleHandlerCount < 0 && (mMessages == null || now < )) { pendingIdleHandlerCount = (); } if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = (mPendingIdleHandlers); } // Run the idle handlers. // We only ever reach this code block during the first iteration. for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { keep = (); } catch (Throwable t) { (TAG, "IdleHandler threw exception", t); } if (!keep) { synchronized (this) { (idler); } } } // Reset the idle handler count to 0 so we do not run them again. pendingIdleHandlerCount = 0; // While calling an idle handler, a new message could have been delivered // so go back and look again for a pending message without waiting. nextPollTimeoutMillis = 0; } }
The code is a bit long, so I don’t plan to omit some of it this time, because there is also a small easter egg in it.
This is the most important thing in the method
nativePollOnce(ptr, nextPollTimeoutMillis);
Simply put, when nextPollTimeoutMillis == -1, the current thread is suspended and the CPU resource is released. When nextPollTimeoutMillis >= 0, the specified time will be delayed to activate the thread once, allowing the code to continue executing. This involves the underlying pipe pipeline and epoll mechanism, so I won’t continue (actually because I can’t continue). This can answer the above question. When there is no message, just let the thread hang, which can ensure that it does not occupy CPU resources while maintaining Looper's dead loop.
Then let's see how the message is retrieved. There is a Message in MessageQueue, and there is a Message member next in the Message class. It can be seen that Message is a single linked list structure. The order of messages is arranged in sequence according to time. Generally speaking, the Message we want to get is the first (the asynchronous message is not considered here. What will be mentioned in the future, and I successfully dug a hole for myself haha). If the current time is greater than or equal to the time specified in the Message, then the message is taken out and returned to Looper. Since the value of nextPollTimeoutMillis is 0 at this time, Looper comes to get the message again after the current message is processed.
If the current time is less than the time specified in Message, set the nextPollTimeoutMillis value for the next wake-up. There is another type of message that is currently no longer available. NextPollTimeoutMillis will be set to -1, which means suspending the thread. Don't worry, it's not that fast yet, then look down.
The following logic is to determine whether there is an IdleHandler at present. If there is no, it will continue. If it is suspended, it will be suspended. If it is delayed, it will be delayed. If there is an IdleHandler, its queueIdle() method will be executed. What does this IdleHandler do? You should be able to guess one or two of the names, so I won’t go into it here. Some of the wonderful uses of it can be read by me beforeAndroid startup optimization for delay loading. After executing the queueIdle() method, nextPollTimeoutMillis will be set to 0 and check again whether there are new messages in the message queue.
Handler
The above has explained the process of getting messages clearly. Everything is ready, and it's almost time to add messages to the message queue. It's time for the Handler we are most familiar with to appear. There are two main ways for Handler to add messages to the queue:
(); ();
The first is mainly to send Messages, and the second is Runnable. Either way, you will eventually enter the enqueueMessage() method of MessageQueue.
boolean enqueueMessage(Message msg, long when) { ... synchronized (this) { ... (); = when; Message p = mMessages; boolean needWake; if (p == null || when == 0 || when < ) { // New head, wake up the event queue if blocked. = 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 && == null && (); Message prev; for (;;) { prev = p; p = ; if (p == null || when < ) { break; } if (needWake && ()) { needWake = false; } } = p; // invariant: p == = msg; } // We can assume mPtr != 0 because mQuitting is false. if (needWake) { nativeWake(mPtr); } } return true; }
Generally speaking, when we send a message through Handler, we will get a boot time through (), and then the MessageQueue will sort the Message based on this time. Therefore, there are two situations in the enqueueMessage() method, one is that it can be inserted directly at the head of the team. One is to be in the middle, you need to traverse it, and then find a suitable pit to insert it. When == 0 corresponds to Handler's sendMessageAtFrontOfQueue() and postAtFrontOfQueue() methods. The function of needWake is to wake up the Looper thread according to the situation.
There is something not mentioned above, that is, after Looper takes out the Message from the MessageQueue, it will hand it over to the Handler for message distribution.
public void dispatchMessage(Message msg) { if ( != null) { handleCallback(msg); } else { if (mCallback != null) { if ((msg)) { return; } } handleMessage(msg); } }
The priority order is the callback that comes with Message, followed by the callback that comes with Handler, and finally the handleMessage() callback.
ThreadLocal
I still remember that there is a ThreadLocal in Looper. I put it to the end because it can be described separately and does not want to interfere with the entire process.
ThreadLocal is a data storage class. The most amazing thing about it is that it is clearly the same ThreadLocal object, but different objects can be stored in different threads. For example, "Hello" is stored in thread A, and "World" is stored in thread B. They do not interfere with each other.
In the Handler mechanism, since a Looper corresponds to a thread, it is most appropriate to store the Looper in ThreadLocal.
The commonly used set() and get() methods for ThreadLocal price comparison. Let’s see how it is implemented separately.
public void set(T value) { Thread t = (); ThreadLocalMap map = getMap(t); if (map != null) (this, value); else createMap(t, value); }
First, get the ThreadLocalMap. If you find it, set the value directly. If you can't find it, create one.
ThreadLocalMap getMap(Thread t) { return ; }
After seeing this, you probably understand. There is a ThreadLocalMap object in each thread. Through the() method, we actually get the ThreadLocalMap in the current thread. Different threads, the ThreadLocalMap obtained is naturally different.
Let’s take a look at what this ThreadLocalMap is from. There is a sentence in the comments of the class:
ThreadLocalMap is a customized hash map suitable only for maintaining thread local values.
From the comments, you can know that this is a custom HashMap, and its Entry class specifies that Key can only be of type ThreadLocal. So just think of it as a HashMap.
The get() method is easy to understand, just to extract the value from the map. Just take a look.
public T get() { Thread t = (); ThreadLocalMap map = getMap(t); if (map != null) { e = (this); if (e != null) { @SuppressWarnings("unchecked") T result = (T); return result; } } return setInitialValue(); }
Written at the end
Although before I started writing, I felt that the Handler mechanism was relatively simple and there was no need to write it, but when I really wanted to write it, I still had to understand the details of the code in depth, and then I found that some places I didn’t understand well enough before. Being able to understand and writing to let others understand is actually different levels.
Okay, the above is the entire content of this article. I hope that the content of this article has a certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support.