Mention message mechanism everyone is not strange, because in the daily development and it is not to deal with, Guo Aunt has also had the relevant chapter to tell: Android asynchronous message processing mechanism completely parsing, take you from the perspective of the source completely understand, can not help but sigh write very good very suitable. To tell the truth, Guo Aunt this article read more than 3 times, and even print out carefully study, every time with notes point things, but are scattered, this combination of “Android development art exploration” to record their learning process.

Android dictates that UI access is only done in the main thread. If the UI is accessed in a child thread, an exception is raised. Why?

Why doesn’t The Android system allow child threads to access the UI? This is because Android UI controls are thread-unsafe, and concurrent access in multiple threads can cause UI controls to be in an unexpected state.

Android message mechanism through the analysis of the above Guo Aunt article is about the same, but this time I again to each break.

How Handler works

Handler provides a series of send() and post() methods, and each of the post() methods eventually calls the send() method to send a message. In a series of the send () method in addition to sendMessageAtFrontOfQueue () method, other methods of sending a message and call to sendMessageAtTime () method, But eventually the enqueueMessage() method of the Handler itself is called. Take a look at these methods:

    // Handler.sendMessageAtTime()
public boolean sendMessageAtTime(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);
   }
// Handler.sendMessageAtFrontOfQueue()
   public final boolean sendMessageAtFrontOfQueue(Message msg) {
       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, 0);
   }
// Handler.enqueueMessage()
   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

The enqueueMessage() method first stores the current handler object: MSG. Target = this, then call MessageQueue’s enqueueMessage() method to queue the message, which sends the message via Handler.

When looper.loop () is called, the message is sent out of the queue, and the final message is handed by Looper to Handler, which calls Handler’s dispatchMessage() method. The dispatchMessage() method is implemented 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 the code

First check if Message’s callback (msg.callback) is null. If not, the Message is processed via handleCallback(). MSG. Callback is a Runnable object, which is essentially a Runnable argument passed through the Handler’s POST method. The logic of the handleCallback() method is also simple:

private static void handleCallback(Message message) {
    message.callback.run();
}Copy the code

So sending a message via post() could be written like this:

Handler handler = new Handler(); Handler.post (new Runnable() {@override public void run() {handler.post(new Runnable() {@override public Void run() {// do UI operations here}}); }; }).start();Copy the code

This gives you a better understanding of the handleCallback() method. Return to the dispatchMessage() method to determine whether mCallback is null. If not, McAllback.handlemessage () is called to process the message.

public interface Callback {
    public boolean handleMessage(Message msg);
}Copy the code

Callback is an interface that can be used to create an instance of Handler without subclassing Handler. Here’s the most intuitive example:

class callback implements Handler.Callback { @Override public boolean handleMessage(Message msg) { // 3. Return false; } } Handler handler = new Handler(new callback()); new Thread(new Runnable() { @Override public void run() { // 1. // 2. Send messages through a series of handler methods}}).start();Copy the code

McAllback.handlemessage () also clears this up. Then dispatchMessage() finally calls Handler’s handlerMessage() method to process the message.

The Handler is now used to send messages and process messages. MessageQueue’s enqueueMessage() method is used to queue messages.

How MessageQueue works

A MessageQueue is called a MessageQueue and consists of two operations: Insert (enqueueMessage) and delete (next), but MessageQueue actually maintains messages internally through a singly linked list data structure, because singly linked lists have an advantage in insert and delete.

EnqueueMessage inserts a message into a message queue. The purpose of next is to take a message from the MessageQueue and remove it from the MessageQueue, because the MessageQueue read itself is accompanied by a delete operation. Let’s look at MessageQueue’s enqueueMessage() method:

boolean enqueueMessage(Message msg, long when) {
    ...
    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) {
            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;
        }
        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}Copy the code

The main operation of the enqueueMessage() method is the insertion of a single linked list. Instead of storing all messages, it uses mMessage to represent the current message to be processed, and then calls msg.next to specify the next message in chronological order.

Loop () calls MessageQueue’s next() method to queue messages out of the queue. See next() :

Message next() { ... int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; 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) { // 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

The next() method has an infinite loop. If there is no message in the MessageQueue, the next() method will block until a new message arrives. If there is an mMessage in the MessageQueue, it is dequeued and removed from the singly linked list, and the next message is assigned to the mMessage.

So MessageQueue is the one that ultimately queues messages in and out of the queue, so look at the implementation of looper.loop ().

How Looper works

Looper acts as a message loop within the Android messaging mechanism. Specifically, it constantly checks for new messages from MessageQueue and processes them immediately if there are new messages, otherwise it will remain blocked. Here is the loop() method:

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; Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; }... msg.target.dispatchMessage(msg); . }}Copy the code

First determine the return value of myLooper(). If the return value is null, an exception is thrown, meaning that the thread does not already have Looper, otherwise proceed.

This is followed by an infinite loop calling Queue.next (), so MessageQueue’s next() method is called from there. Every time a message is sent out of the queue, it is passed to the dispatchMessage() method of msg.target, which we already mentioned is actually a Handler, Messages sent through this Handler are then returned to its own dispatchMessage() method.

The only time to break out of the loop is when MessageQueue’s next() method returns NULL. Looper provides two ways to quit() and quitSafely() to quit a Looper. The quitSafely() method, on the other hand, simply sets an exit marker and exits safely when all the existing messages in the message queue run out.

When Looper’s quit() or quitSafely() method is called, MessageQueue’s quit() method is called internally to tell the MessageQueue to quit, MessageQueue’s next() method returns NULL. Therefore, it is recommended to terminate Looper when no longer needed by using the quit() or quitSafely() method.

Having said myLooper(), now look at the myLooper() method:

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}Copy the code

What is sthreadlocal.get ()? Keep reading:

How ThreadLocal works

ThreadLocal is an internal data store class. The same ThreadLocal object has different values in different threads.

Get (Looper) sthreadLocal.get (); Looper sthreadLocal.get (); If it does not exist, an exception is thrown. The prepare() method is called precisely because it internally sets Looper for the current thread with sthreadLocal.set (new Looper()).

Here’s a quick example of what ThreadLocal can do:

public class ThreadLocalActivity extends AppCompatActivity { private static final String TAG = "ljuns"; private ThreadLocal mThreadLocal = new ThreadLocal<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_thread_local); // mthreadlocal.set (true); Log.d(TAG, "main : " + mThreadLocal.get()); New Thread(new Runnable() {@override public void run() {mthreadLocal-set (false); Log.d(TAG, "thread 1 : " + mThreadLocal.get()); } }).start(); @override public void run() {log.d (TAG, "Thread 2:" + mthreadLocal.get ()); } }).start(); }Copy the code

Create a ThreadLocal object (mThreadLocal), and then operate on it in three separate threads. The mThreadLocal for the main thread is true, the mThreadLocal for child thread 1 is false, and the mThreadLocal for child thread 2 should be null because there is no assignment. What are the actual results? Take a look at:

The result, of course, is predictable, because it’s been tried out before, and that’s the wonder of ThreadLocal. With that in mind, let’s look at its internal implementation, starting with the set() method:

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}Copy the code

This method is neat: it first fetches the current thread, and then fetches ThreadLocal data for the current thread through values(). Why do you say that? Let’s start with the values() method:

Values values(Thread current) {
    return current.localValues;
}Copy the code

What are localValues? Private Object[] table, which is the real data structure. ThreadLocal is stored in this table array. So values() actually gets the localValues object, and the table array inside localValues actually owns ThreadLocal’s data.

Back in set(), if we get values that are null, we initialize them, and then we store them through put(). Moving on to the put() method:

void put(ThreadLocal key, Object value) { cleanUp(); int firstTombstone = -1; for (int index = key.hash & mask;; index = next(index)) { Object k = table[index]; if (k == key.reference) { table[index + 1] = value; return; } if (k == null) { if (firstTombstone == -1) { table[index] = key.reference; table[index + 1] = value; size++; return; } table[firstTombstone] = key.reference; table[firstTombstone + 1] = value; tombstones--; size++; return; } if (firstTombstone == -1 && k == TOMBSTONE) { firstTombstone = index; }}}Copy the code

Focusing on the storage rules for the PUT () method, you can see that the value of ThreadLocal is located next to reference in the table array. For example, if the index of reference in the table array is index, the value of ThreadLocal will be stored at the index + 1 position in the table array.

After looking at the set() method, let’s look at the get() method:

@SuppressWarnings("unchecked") public T get() { // Optimized for the fast path. Thread currentThread = Thread.currentThread(); Values values = values(currentThread); if (values ! = null) { Object[] table = values.table; int index = hash & values.mask; if (this.reference == table[index]) { return (T) table[index + 1]; } } else { values = initializeValues(currentThread); } return (T) values.getAfterMiss(this); }Copy the code

The get() method also fetches the current localValues object, and if the object is not null, fetches its table array and finds the location of reference in the table array. Select * from ThreadLocal; select * from ThreadLocal; select * from ThreadLocal; The initial value is returned if the localValues object is null, which is null by default.

ThreadLocal’s set() and get() methods operate on the current thread’s table array, thus confirming the description of ThreadLocal. Once you understand ThreadLocal, you’ll have a better understanding of how Looper works.

I’ve almost walked through Android’s messaging mechanism here, and finally posted a sequence diagram to understand the process (I’m just starting to learn how to use sequence diagrams) :

It is clear from this sequence diagram that a thread corresponds to a Looper object, and a Looper object corresponds to a MessageQueue object. Why is it possible to manipulate the UI using asynchronous message processing? This is because the Handler is always attached to the thread on which it was created. So a time-consuming operation in the child thread sends a message through the Handler, which eventually goes to the main thread for UI operations in the Handler’s handlerMessage() method.