preface

There’s a lot of information about this on the Internet. Why do I have to write it here? Understanding why handler-message-Looper is a clever design, and understanding why it’s done can help us when we’re designing programs. This article explains how to combine phenomena and principles to analyze the nature of event-driven, and I believe that even students who have not read the relevant knowledge can quickly understand. Through this article you will know:

1, how to do thread switch 2, message loop principle 3, child thread message loop 4, linked list implementation stack and queue

thread

  • Q1 Why is thread switching required

1, multithreading can improve the efficiency of concurrent, the introduction of multithreading is bound to have a thread switch 2, multithreading, at the same time the introduction of thread safety problem, the Android for safety, simply rendering UI, specified in drawing the UI only operate in a fixed thread, the thread on the app is created when the start, we call it the UI thread (also known as the main line, For simplicity, the follow-ups are collectively called main City). The non-main thread is called the child thread, and when we are doing time-consuming operations in the child thread and we are ready to display data, we need to switch to the main thread to update the UI

For thread safety, take a step closer to understanding the power of Java Volatile

  • Q2 How do I switch threads

How do you create and start a thread

Thread t1 = new Thread(new Runnable() { @Override public void run() { doTask1(); }}); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { doTask2(); }}); t2.start();Copy the code

As shown above, t2 needs to wait for T1 to complete before executing, so how does T2 know that T1 has finished? There are two ways

1. T1 actively tells T2 that it has finished, which belongs to waiting notification mode (Wait&notify, condition await&signal). 2. T2 continuously checks whether T1 has finished, which belongs to polling mode

If T1 is the child thread and T2 is the main thread, how does T1 notify T2? The first method is wait-notification, which is implemented by handler-message-looper.

Handler to send Message

A classic example

Button button; TextView tv; private Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(@NonNull Message msg) { tv.setText(msg.arg1 + ""); return false; }}); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = findViewById(R.id.start_thread_one); tv = findViewById(R.id.tv); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(()->{ Message message = Message.obtain(); message.arg1 = 2; handler.sendMessage(message); }).start(); }}); }Copy the code

The main thread constructs the handler, the child thread constructs the message, sends the message, executes the handleMessage method in the main thread to update the UI. Obviously, using handler to switch threads requires only two steps:

The main thread sends a message telling the main thread to execute the callback method

You might be wondering: How does the main thread know to execute the handleMessage method? Since the child thread sent the message, there should be a place for the main thread to receive the message. It seems that the messenger is message, and then enter the source code to find out. Android Studio with Android SDK source code (Windows&Mac)

Message analysis

Since message acts as a messenger, it should have the necessary fields to store the data it carries, distinguish the sender from the sender, and so on. Actually, Message has more than these fields. In the previous example, we got the Message instance statically.

private static Message sPool; public static Message obtain() { synchronized (sPoolSync) { if (sPool ! = null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); }Copy the code

SPool is a static variable that, as its name implies, is the Message storage pool. Obtain () obtain() searches for available messages from the storage pool, creates new messages, and returns them. When do you place messages into the storage pool? We just need to pay attention to the growth of sPoolSize and search to find:

void recycleUnchecked() { // Mark the message as in use while it remains in the recycled object pool. // Clear out all other details. flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = UID_NONE; workSourceUid = UID_NONE; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; }}}Copy the code

Message contains a next field of type message. Those familiar with lists should know that this is the basic operation of lists. This field is used to point to the next node in the list. Now put a Message object into the message storage pool:

Place the message Next field to the header of the storage pool, which points to the first element, place the header of the storage pool to the message to be inserted, and add +1 to the number in the storage pool. At this point, message has been put into the storage pool and is used as the head of the storage pool.

Let’s go back to retrieving messages from the storage pool:

Declare a message reference that points to the head of the storage pool, points the head of the storage pool to the next element in the list, and empty Message Next with the number -1 in the storage pool, at which point Message has been fetched from the storage pool.

Each time we put message in the header, we fetch it from the header, which is the familiar stack structure. That is, the Message object storage pool is implemented on a stack.

Question 1

At this point, we have analyzed the structure of the Message storage pool, leaving two questions for further analysis:

1. We obtain message using password, and when do we call recycleUnchecked() to add it to the storage pool? 2. Can messages be constructed directly?

MessageQueue

SendMessage (message) :

    public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
Copy the code

The sendMessageAtTime() method takes two arguments, the message object to be sent and the time to delay the message execution. A new type is introduced: MessageQueue, which declares a field: mQueue. Then call enqueueMessage, where msg.target = this, target actually points to handler. Queue. EnqueueMessage (MSG, uptimeMillis);

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 | | the when = = 0 | | the when < p.w hen) {/ / New head, wake up the event queue if blocked. / / the first MSG. The 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 ! // if (needWake) {nativeWake(mPtr); }}Copy the code

MessageQueue is exactly what it sounds like: a Message queue. How does it implement queues? There is a mMessages field of type Message. Yes, MessageQueue is linked to a list by message, but the list implements queues. Let’s see how to implement queues. EnqueueMessage source code marks the first, second, third three key points.

The first point

The message Next field points to the queue head and the queue head points to the message if the queue is empty, or the latency is 0, or the latency is less than the queue head node. At this point, the message is inserted into the queue head of enqueueMessage.

The second point

If the first point is not true, then Message cannot be inserted into the queue head, so the search continues in the queue until it finds a node with a delay longer than Message or reaches the end of the queue, and then message is inserted into the queue.

As you can see from points 1 and 2, enqueueMessage implements queues and sorts them from smallest to largest message latency, meaning queue headers have the smallest latency and need to be executed first.

The third point

Question 2

Wake up the MessageQueue queue for subsequent analysis.

When do I fetch message

So far, handler.sendMessage(message) process of sending message to MessageQueue has been analyzed, so when will the message in MessageQueue be retrieved? Since there is enqueueMessage in MessageQueue, try to see if there is a queuemessage, unfortunately there is not. Then we have reason to believe that the MessageQueue mMessages field may have been operated somewhere. After searching for it, we found that there were many references and it was difficult to analyze. For another thought, where is the MessageQueue object mQueue referenced?

    public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
Copy the code

We find that mQueue refers to mQueue in mLooper, and mLooper = looper.mylooper (). Now that we’ve cast our skeptical eyes on Looper, who is Looper? Let’s move on.

Which analysis

Start with looper.mylooper ()

##Looper.java @UnsupportedAppUsage static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); /** * Return the Looper object associated with the current thread. Returns * null if the calling thread is not associated with a Looper. */ public static @Nullable Looper myLooper() { return sThreadLocal.get(); } ##ThreadLocal.java 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

As you can see, the Looper object is stored in the current thread, leaving ThreadLocal aside for the moment, and looking at the GET method, we have reason to believe that there is a set method, and sure enough.

public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map ! = null) map.set(this, value); else createMap(t, value); }Copy the code

Look for its references. This method has a lot of references. Since the ThreadLocal get method is called in Looper. Java, is the ThreadLocal set method also called in Looper. Java?

Looper.java private static void prepare(boolean quitAllowed) { 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

Look for the prepareMainLooper() method reference

private static Looper sMainLooper; // guarded by Looper.class private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() ! = null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }Copy the code

In the prepare method, a Looper object is constructed. Now we see that the main thread mQueue has been constructed, which means that the main thread starts with a Looper object and stores it in the static variable sThreadLocal. The static variable sMainLooper also points to the newly constructed Looper object. We then look for the reference to mQueue and find

Messagequeue.java next() Message prevMsg = null; 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()); } 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; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { // No more messages. nextPollTimeoutMillis = -1; }Copy the code

Message MSG = mMessages. We have analyzed before that after the handler sends the Message, it finally hangs the Message on mMessages, which is our Message queue. The next method takes the message from the queue header and removes it from the queue. At this point, we have found the message queue entry time and mode, message queue exit mode. What about message queue exit time? That’s when it was called. Continue backtracking with the Looper loop() method mentioned above.

for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } // This must be in a local variable, in case a UI event sets the logger final Printer logging = me.mLogging; if (logging ! = null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } try { msg.target.dispatchMessage(msg); if (observer ! = null) { observer.messageDispatched(token, msg); } dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } catch (Exception exception) { if (observer ! = null) { observer.dispatchingThrewException(token, msg, exception); } throw exception; } finally { ThreadLocalWorkSource.restore(origWorkSource); if (traceTag ! = 0) { Trace.traceEnd(traceTag); } } if (logging ! = null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } msg.recycleUnchecked(); }Copy the code

It is obvious that the loop() method has an infinite loop that exits when there is no message or other exception in the queue.

msg.target.dispatchMessage(msg); MSG. Target is the handler we mentioned earlier, handler.dispatchMessage(MSG)

public void dispatchMessage(@NonNull Message msg) { if (msg.callback ! = null) { handleCallback(msg); (1) } else { if (mCallback ! = null) { (2) if (mCallback.handleMessage(msg)) { return; } } (3) handleMessage(msg); }}Copy the code

There are three methods in this method:

HandleCallback () calls a callback field of type Runnable in message. The view.post(Runnable) method is used to get the view width and height, where Runnable is assigned to the callback field. The handler mCallback field is the interface containing the handleMessage() method, whose return value determines whether to continue distributing the message. MCallback is optionally implemented, so that if you don’t want to override the handler handleMessage() method, you can override the callback method. HandleMessage, which needs to be overridden when subclassing handler.

The application of points 1, 2 and 3 above may also cause some confusion, so let’s use examples to illustrate. 1, the handleCallback ()

Handler. PostDelayed (new Runnable() {@override public void run() {}}, 100); Tv.post (new Runnable() {@override public void run() {}});Copy the code

McAllback. handleMessage(MSG) constructs an anonymous inner class instance of Handler

private Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(@NonNull Message msg) { tv.setText(msg.arg1 + ""); return false; }});Copy the code

The inner class inherits the Handler and overrides the handleMessage method

class MyHandler extends Handler { @Override public void handleMessage(@NonNull Message msg) { tv.setText(msg.arg1 + "");  } } MyHandler handler = new MyHandler();Copy the code

At this point, we already know the message execution timing in MessageQueue. You may be wondering how to ensure that the above runnable and handleMessage are executed in the main thread. Call stack: loop()->msg.target.dispatch()->runnable/handleMessage(). We also notice that loop() has two print statements:

if (logging ! = null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); } msg.target.dispatchMessage(msg); if (logging ! = null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); }Copy the code

As you can see before and after dispatchMessage(), this provides a convenience: the main thread message execution time is calculated by monitoring the output of the statement.

Msg.recycleunchecked () in loop(). Now answer question 1

Get instance from the storage pool using obtain(). After message is used in loop(), call msg.recycleunchecked () and add it to the storage pool (stack). 2. Message objects can be not only obtained (), but also constructed directly. Ultimately, they are placed into a storage pool, which is recommended to use Obtain () to better reuse message.

MessageQueue next () analysis

Earlier we walked briefly through the next() method, which fetched the Message object from MessageQueue. Loop () gets message through the next call. If message is empty, loop() exits. Assuming that happens, the main thread exits and the app is unplayable. So by design, the message returned by Next cannot be empty, so it needs to keep checking for messages until it gets a valid message, so next might block. In fact, next uses a for loop that does block, so let’s look at the details:

for (;;) { if (nextPollTimeoutMillis ! = 0) { Binder.flushPendingCommands(); } // nativePollOnce(PTR, nextPollTimeoutMillis); synchronized (this) { // Try to retrieve the next message. Return if found. final long now = SystemClock.uptimeMillis();  Message prevMsg = null; Message msg = mMessages; if (msg ! = null &&msg.target == null) {//第二 段 // a barrier. Find the next asynchronous message in the queue.do { prevMsg = msg; msg = msg.next; } while (msg ! = null && ! msg.isAsynchronous()); } if (msg ! 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 {// get a message. mBlocked = false; if (prevMsg ! = null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); 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 < mMessages.when)) { pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) {idle handlers to run.loop and wait some more. continue; }}}Copy the code

The source code marked 6 key points, one by one analysis.

1. NativePollOnce (PTR, nextPollTimeoutMillis) finds whether there is data in the native layer MessageQueue. NextPollTimeoutMillis =0 This method returns immediately; NextPollTimeoutMillis = 1; The method blocks until it is actively woken up; NextPollTimeoutMillis > 0; Returns after blocking for a specific time. When a message is queued, call the following code:

            if (needWake) {
                nativeWake(mPtr);
            }
Copy the code

NeedWake =mBlocked. When blocked, mBlocked=true. After nativeWake(mPtr) is executed, >nativePollOnce() is returned. That’s why we said handler-Looper used wait for notification in the first place. 2. If the queue head node is a barrier message, traverse the queue until an asynchronous message is found, otherwise the loop continues until nativePollOnce blocks. When invalidate the view, insert a barrier message (delayTime=0) into the queue header to ensure that the UI is drawn in a timely manner. If a node in the queue has the same delay time as the node to be inserted, the node to be inserted is placed behind the existing node, and the barrier message is always inserted first. After the barrier message is inserted, subsequent refresh messages are set to asynchronous and then inserted into the queue, and this message is preferentially executed. 3. If the execution time is not reached, set the blocking timeout period. 4. Fetch the Message node and adjust the queue header. 6. Set the blocking flag bit mBlocked=true and continue the loop

Handler Thread switching nature

The handler sends the message above, and the whole process has been analyzed until the message is consumed. To visualize the process, use a flowchart.

Message object storage pool

Into the stack

MessageQueue queue

Out team

The call stack

Send message call stack

handler.sendMessage(message)

sendMessage->sendMessageDelayed(msg, 0)->sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)->enqueueMessage(queue, msg, uptimeMillis)->queue.enqueueMessage(msg, uptimeMillis)

Fetch message call stack

msg = loop()->queue.next()

msg->dispatchMessage(msg)->handleCallback(msg)/handleMessage(msg)

Handler, MessageQueue and Looper are shown as follows:

Handler to Looper

How do I ensure that handleMessage(MSG) is executed on the main thread? In other words, how do you switch to the main thread after receiving the message?

The thread executing loop() is the thread executing handleMessage(MSG), and loop() is executed inside main(). 2. The message sent by Handler is stored in the Handler’s Messagequeue queue, which points to Looper’s Messagequeue queue. The loop() method retrieves the message from Looper’s Messagequeue. Therefore, Handler and Looper share the same queue, Handler sending (queue entry) and Looper receiving (queue exit), and thread switching is implemented via the intermediate bridge messagequeue. 3. Next, how do Handler and Looper share queues? When a Handler is built, the stored Looper is retrieved for the current thread and its Messagequeue is retrieved. The LoOPER object needs to be constructed in the same thread and the Looper reference set to the current thread. When the app starts, the system has assigned the main thread looper to the static variable sMainLooper. We can get the main thread looper object by getMainLooper(). When handler constructs, you can choose whether to pass in the looper object or not. If not, the looper object is fetched from the current thread by default. This is why you can’t simply use message loops in child threads, because the handler is not associated with looper.

Child thread message loop

Above, we analyze that the child thread sends messages, and the main thread executes in polling. Can we send messages on the main thread (other threads) and poll the child threads? There are practical requirements for this scenario:

Thread A constantly pulls data from the network, and the data is divided into several types. After receiving the data, it needs to be classified and processed, which may be time-consuming. In order not to affect the efficiency of thread A, it needs to be processed by thread B, which has been cycling through the data pulled by thread A.

If this sounds familiar, we want thread B to perform looper like the main thread, so let’s see what we can do.

Handler handlerB = null; Thread b = new Thread(new Runnable() {@override public void run() {// Create a looper object and save a reference to the current Thread. HandlerB = new Handler(new Callback() {@override public Boolean handleMessage(@nonnull) Message msg) { return false; }}); // The child thread opens loop() looper.loop (); }}); b.start(); Thread a = new Thread(()->{handlerb. sendMessage(new Message()); }); a.start();Copy the code

Source code has been annotated, no longer superscript. You might say, is it always this much trouble? In fact, Android developers have taken this into account by encapsulating the HandlerThread class to implement a message loop for child threads in a similar way. The point is to associate Handler with Looper.

conclusion

The Handler, Message, Looper principles and their relationships are analyzed from the source and application perspectives. Message loops are widely used in Android development, and we should not only be able to use them freely, but also learn from the source code.