preface
When it comes to Android messaging, the main thing is how the Handler works. This includes MessageQueue and Looper processes.
To begin the body, two questions:
- Why do UI updates happen in the main thread?
- Why doesn’t the main thread in Android cause
Looper.loop()
Is the loop stuck?
The determination of the UI thread is done in the checkThread method in ViewRootImpl.
Here’s a simple answer to the first question:
If the UI can be modified in child threads, the concurrent access of multiple threads may lead to the unpredictability of UI controls, and the use of locking will reduce the efficiency of UI access and block the execution of other threads. Therefore, the most simple and effective method is to use a single thread model to deal with UI operations.
The Handler cannot run without the support of the underlying MessageQueue and Looper. MessageQueue is translated as a Message queue, which stores the messages needed by the Handler. MessageQueue is not a queue, but actually uses a single linked list data structure to store messages.
So how does Handler get the Message? This is where Looper comes in. Looper starts an endless loop through looper.loop (), fetching messages from MessageQueue and passing them to Handler.
Another point to make here is to get Looper, which is to raise a storage class: ThreadLocal
How ThreadLocal works
ThreadLocal is an internal data store class that can store data from a thread that is not accessible to other threads. Let’s see if that’s true in principle.
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) map.set(this, value); else createMap(t, value); } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e ! = null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }Copy the code
You can see that its set and GET methods are the operations done in the current thread, and inside ThreadLocalMap is an array table. This ensures that data in different threads does not interfere with each other.
ThreadLocal is used for complex scenarios such as listener passing, in addition to fetching Looper from Handler.
Now that we have a brief introduction to ThreadLocal, let’s walk through the message mechanism step by step with New Handler().
How Looper works
// Handler.java public Handler() { this(null, false); } // callback message callback; Async Whether to synchronize public Handler(Callback Callback, Boolean async) {... MLooper = looper.mylooper (); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); MessggeQueue mQueue = mlooper.mqueue; mCallback = callback; mAsynchronous = async; }Copy the code
We normally use the parameterless method, which passes an empty callback and false.
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
Copy the code
This is where the ThreadLoacal class comes in, so when was the looper value set?
The prepare method and prepareMainLooper method are prepared and prepareMainLooper methods.
public static void prepare() { prepare(true); } private static void prepare(Boolean quitAllowed) {// Before creating looper, check whether looper is bound to ThreadLoacal. Prepare can only be set once. if (sThreadLocal.get() ! = null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } public static void prepareMainLooper() {prepare(false); synchronized (Looper.class) { if (sMainLooper ! = null) { throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); }}Copy the code
The prepare method can only be set once, so why can we use it directly in the main thread? The entry to the app is in the Main method of the ActivityThread:
public static void main(String[] args) { ... //1. Initialize the Looper object looper.prepareMainLooper (); // 2. Open infinite looper.loop (); throw new RuntimeException("Main thread loop unexpectedly exited"); }Copy the code
See, initialization is here, so let’s look at looper’s initialization method again:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
Copy the code
Initialization of Looper does two things: create the MessageQueue MessageQueue and retrieve the current thread. From here, we can draw a conclusion:
- The prepare method can only be called once in a thread.
- Looper’s initialization can only be called once in a thread.
- Finally, it can be learned that a thread corresponds to a Looper, and a Looper corresponds to a MessageQueue.
Looper can be understood as a factory line that keeps retrieving messages from MessageQueue. The factory line is opened by looper.loop ().
public static void loop() { final Looper me = myLooper(); If (me == null) {throw new RuntimeException("No looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; . //2. Start an infinite loop for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; }... try { msg.target.dispatchMessage(msg); dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } finally { if (traceTag ! = 0) { Trace.traceEnd(traceTag); }}... }}Copy the code
Which method by opening an infinite loop, and constantly to the Message from MessageQueue news, when the Message is empty, exit the loop and call or MSG. Target. DispatchMessage (MSG) method, Target is the MSG bound Handler object.
How this Handler works
Okay, so here we are back in the Handler class.
public void dispatchMessage(Message msg) { if (msg.callback ! = null) { handleCallback(msg); } else { if (mCallback ! = null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); }}Copy the code
This handleMessage is the method we need to implement. So how is the Handler set up in the Message? Let’s look at the familiar sendMessage method:
public final boolean sendMessage(Message msg) { return sendMessageDelayed(msg, 0); } public final boolean sendMessageDelayed(Message msg, long delayMillis) { if (delayMillis < 0) { delayMillis = 0; } return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis); } public boolean sendMessageAtTime(Message msg, long uptimeMillis) { MessageQueue queue = mQueue; . return enqueueMessage(queue, msg, uptimeMillis); } private Boolean enqueueMessage(MessageQueue queue, Message MSG, long uptimeMillis) { msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }Copy the code
As you can see, the enqueueMessage assigns the handler to the target of the MSG through a series of methods. The last call is in MessageQueue’s enqueueMessage method:
boolean enqueueMessage(Message msg, long when) { if (msg.target == null) { throw new IllegalArgumentException("Message must have a target."); } if (msg.isInUse()) { throw new IllegalStateException(msg + " This message is already in use."); } synchronized (this) { if (mQuitting) { IllegalStateException e = new IllegalStateException( msg.target + " sending message to a Handler on a dead thread"); Log.w(TAG, e.getMessage(), e); msg.recycle(); return false; } 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 { 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) { nativeWake(mPtr); } } return true; }Copy the code
The enqueueMessage method does two main things:
First determine whether the handler exists and is in use. It is then inserted into MessageQueue in chronological order.
Looper.loop() is an infinite loop, so why doesn’t it block the main thread?
Let’s look at MessageQueue’s next method:
Message next() { final long ptr = mPtr; if (ptr == 0) { return null; } int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { nativePollOnce(ptr, nextPollTimeoutMillis); . }}Copy the code
NativePollOnce method is a native method. When calling this native method, the main thread will release CPU resources and go to sleep until the next message arrives or a transaction occurs. Data is written to the write end of pipe pipe to wake up the main thread. The epoll mechanism used here. For detailed analysis of nativePollOnce, please refer to nativePollOnce function Analysis
conclusion
- The app starts from the ActivityThread
main
Method start, passLooper.prepare()
Conduct Looper as wellMessageQueue
And the creation ofThreadLocal
Binding between threads. - When we create a Handler, we use ThreadLocal to get the Looper in the thread and the MessageQueue bound to the Looper.
- through
Handler.sendMessage()
Method to bind MSG to Handler, and then insert MSG into MessageQueue in chronological order. - After the main thread is created, looper.loop () starts an endless loop, fetching messages from Looper’s existing MessageQueue and then calling the Handler’s non-empty Message binding
dispatchMessage(msg)
Method, which will eventually call the handlerMessage method that we copied.
Now to answer the second question we asked at the beginning: why doesn’t the main thread in Android freeze because of the dead loop in looper.loop ()?
1. The cause of the deadlock is that CPU resources are occupied by time-consuming operations performed on the main thread, such as ANR, looper.loop () itself does not cause this. 2. Looper.loop() Releases CPU resources when there is no message. 3. App processes need to be in an infinite loop; otherwise, App processes end.
The resources
Exploring the Art of Android Development