preface

Handler mechanism as the basis of the Android Framework layer, a lot of problems need to study the source code to figure out, if just stay in the recitation of some interview answers is no better code understanding. So I want to study Handler source code in combination with interview questions.

The content of this article is mainly divided into the following aspects:

  1. We’ve all used the Handler mechanism before, so we’ll start by analyzing the end of the message sent by the Handler, the MessageQueue#enqueueMessage method
  2. Analyze the types of messages processed in MessageQueue (synchronous, asynchronous, barrier messages) and the next method
  3. Finally, the details of Handler mechanism are analyzed in the form of interview questions.

Add the message MessageQueue#enqueMessage

All methods in Handler that send messages will eventually call Handler#enqueueMessage:

//Handler#enqueueMessage private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, MSG. Target = this; long uptimeMillis) {// Specify the Target object of the Message to be sent to the current Handler MSG. msg.workSourceUid = ThreadLocalWorkSource.getUid(); if (mAsynchronous) { msg.setAsynchronous(true); } // MessageQueue#enqueMessage return queue. Enqueue (MSG, uptimeMillis); }Copy the code

When we write a Handler, we’ll overwrite the empty handleMessage method in the Handler. Handler#enqueueMessage ensures that the sent message is handled by our override method by writing the message target object to the current Handler.

// MessageQueue#enqueMessage boolean enqueueMessage(Message msg, long when) { // .... Synchronized (this) {//.... // mMessages is a single linked list with no head nodes. However, in enqueMessage, only when messages are added to mMessages is Message p = mMessages; boolean needWake; / / -- -- -- -- -- -- -- is inserted into the beginning -- -- -- -- -- -- -- -- the if (p = = null | | the when = = 0 | | the when < p.w hen) {/ / Message to join / / in the following cases, using interpolation method, a new head node / / 1. Queue (single linked list) empty // 2. When =0, the first time // 3. The header of the current queue is less than the Message msg.next = p; mMessages = msg; needWake = mBlocked; } else {/ / -- -- -- -- -- -- -- -- is inserted into the middle part -- -- -- -- -- -- -- -- -- needWake = mBlocked && p.t arget = = null && MSG. IsAsynchronous (); Message prev; // traverses the list for (;;) { prev = p; p = p.next; / / find can be inserted into the position of the if (p = = null | | the when < p.w hen) {break; } if (needWake && p.isAsynchronous()) { needWake = false; }} // insert node MSG. Next = p; // invariant: p == prev.next prev.next = msg; If (needWake) {nativeWake(mPtr); if (needWake) {nativeWake(mPtr); } } return true; }Copy the code

From the key method enqueMessage of MessageQueue, we can get the following key information: mMessage is in the form of a single linked list in data structure, but MessageQueue will sort according to the time of Message. The head insertion method is used, and the smaller the when, the earlier the time

Consume message MessageQueue#next

Message type

Message messages fall into three categories: synchronous, asynchronous, and barrier messages.

Synchronous and asynchronous messages

The messages we usually use are synchronous messages:

Constructor public Handler(@nonnull Looper Looper) {// Async (Looper, null, false); }Copy the code

Our regular build handlers specify the async field to false, which indirectly modifiers the global variable mAsynchronous, and a synchronous message is added to the queue.

// Handler#enqueueMessage private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); // judge if (mAsynchronous) {msg.setasynchronous (true); } return queue.enqueueMessage(msg, uptimeMillis); }Copy the code

To send asynchronous messages, you need to set async to true.

Barrier message

Both synchronous and asynchronous messages specify a message.target processing object, but barrier messages set target to null. So a barrier message can be understood as a special node in a message queue.

A barrier message prevents MessageQueue#next from retrieving the synchronization message. But it does not prevent fetching asynchronous messages. Asynchronous messages have higher priority in the presence of message barriers.

Since MessageQueue is a priority queue that is sorted by time, synchronous messages queuing by time will affect the processing of messages.

There is a VSync message in the Android system, which is mainly responsible for updating the screen display every 16ms. If the user synchronization message is not completed within 16ms, the update operation of the VSync message cannot be performed. In the view of the user, there will be frame drop or lag. The Android development requires that execution of each message be limited to 16ms. However, there may be multiple synchronization messages in the message queue. If there are 10 synchronization messages in the main thread message queue, each synchronization message needs to be executed in 10ms, so the total execution needs to be executed in 100ms. During this period, nearly 7 frames cannot be displayed properly.

MessageQueue#next

Message next() { // ... / / -- -- -- -- -- -- -- -- -- -- -- -- -- take message loop -- -- -- -- -- -- -- -- -- -- -- -- -- -- int nextPollTimeoutMillis = 0; for (;;) {/ /... / / -- -- -- -- -- -- -- -- -- -- -- -- -- to sleep -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- nativePollOnce (PTR, nextPollTimeoutMillis); synchronized (this) { // .... // ------------- fetch message logic ----------------}}Copy the code

The logic of MessageQueue#next is pretty clear, broken down into these parts.

Take out the message

### MessageQueue#next Message next() { // ..... int nextPollTimeoutMillis = 0; for (;;) {/ /... nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { Message prevMsg = null; Message msg = mMessages; MSG. Target == null if (MSG! = null) is the first object in a MessageQueue = null && msg.target == null) { do { prevMsg = msg; msg = msg.next; } while (MSG! = null && ! msg.isAsynchronous()); } // whether synchronous or asynchronous message if (MSG! NextPollTimeoutMillis = (int) math.min (MSG. When) {// Block the thread until time nextPollTimeoutMillis = (int) math.min (MSG. Integer.MAX_VALUE); } else { // Got a message. mBlocked = false; // Get message 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; } // Related to Idlehandler}}Copy the code

Message#next checks if there are barrier messages when retrieving messages, as shown in the figure. If there are barrier messages, synchronous messages are not processed first, and asynchronous messages are processed first.

When a message is fetched but does not arrive, the thread is temporarily blocked using the nativePollOnce method. This approach works with nativeWake in the MessageQueue#enqueMessage method. Both methods are native, and ativePollOnce uses the Epoll mechanism in Linux to listen for file descriptors, which nativeWake writes to.

Reuse message

After the Message is fetched from Looper#loop, it is reused:

msg.recycleUnchecked();
Copy the code
// MessageQueue#recycleUnchecked 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

In the reuse method, the Message field is reinitialized and put into the reuse pool sPool:

private static Message sPool;
Copy the code

Message itself can also be thought of as the structure of a linked list, with sPool being the head of the list. When using Message, we should use the obtain method to reuse objects in the Message pool to minimize memory jitter associated with creating and destroying objects.

// Message#obtain 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

Look at the source code with a problem

Q1: Do handlers created in the same thread share a MessageQueue?

A1: yes. For the answer to this question, check out Looper#prepare:

Looper#prepare

private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() ! Throw new RuntimeException("Only one Looper may be created per thread"); } // Create a new Looper and set it to sthreadLocal. set(new Looper(quitAllowed)); }Copy the code

ThreadLocal can be thought of as a Map structure: Map Different threads correspond to different instances of Looper objects (if created). With the Looper constructor, a new MessageQueue object is instantiated at each Looper creation. ,>

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
Copy the code

Therefore, Looper, MessageQueue and Thread are all in one-to-one correspondence. A thread has only one Looper. More importantly, Looper#prepare checks and throws an exception if Looper is created repeatedly. This ensures that one Thread corresponds to one Looper.

Extension question: How does MessageQueue keep threads safe?

  1. If no Looper is specified to create a Handler, each Handler uses the current thread-specific Looper and thus has its own MessageQueue. In this way, all the handlers created in the same child thread share a MessageQueue, so there is no need to worry about thread safety in the serial case.
  2. If Looper is specified, but handlers are created in a different child thread, thread-safety considerations are required. MessageQueue#enqueueMessage MessageQueue#next
//MessageQueue#enqueueMessage boolean enqueueMessage(Message msg, long when) { // ..... synchronized (this) { // ..... } return true; } // MessageQueue#next Message next() { // .... for (;;) {/ /... synchronized (this) { // ... }}}Copy the code

Both methods use synchronized, and the object of the lock is MessageQueue itself. Because these handlers share the same Looper, or MessageQueue, messages can be added to ensure that they are written and read in the set order.

Extended thinking: while ensuring thread safety, it is difficult to guarantee message immediacy

All methods in Handler that send messages after a delay use the current time +delay time:

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
Copy the code

If no time is specified, delay is set to 0:

public final boolean post(@NonNull Runnable r) {
   return  sendMessageDelayed(getPostMessage(r), 0);
}
Copy the code

MessageQueue sorts the messages in chronological order. However, since MessageQueue may be accessed by handlers on different threads holding the same Looper object, this immediacy is not guaranteed due to the locking mechanism that queues up incoming visitors. It will be later than the specified time.

Q2: Handler memory leaks and their handling?

A1: Memory leaks caused by handlers should be thought of in two ways

  1. Handler, if it is a non-static inner class of an Activity, implicitly holds a reference to the outside Activity
  2. Methods such as Handler#postDelayed send messages that are executed long after.

The second question is:

// Handler#enqueueMessage
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

As you can see from the code above, Message specifies Target as the current Handler. So Message holds a reference to Handler and is put into MessageQueue to be woken up. In combination with Q1 and Q2, several important classes in the Handler processing mechanism and their corresponding relationships are shown in the figure below:

Important approaches to Handler mechanism design are summarized in the following figure:There can be more than one Message in a MessageQueue, and a Handler object is specified when a Message is put into a MessageQueue (Handler#enqueMessage). A Thread is managed by a ThreadLocal and has only one Looper. The Looper creates a MessageQueue using Looper#prepare. Looper starts the loop with a loop and retrieves the message (messagequeu # Next).

handling

  1. Isolate Handler as a class, use static modifier, and use weak references to modify the Activity
  2. Remove all messages and callbacks in the Activity life cycle

Specific code can refer to:

private CurHandler handler = new CurHandler(this);

private static class CurHandler extends Handler {
    WeakReference<Activity> outer;
    public CurHandler(@NonNull Activity outer){
        super(outer.getMainLooper());
        this.outer = new WeakReference<Activity>(outer);
    }

    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    handler.removeCallbacksAndMessages(null);
}
Copy the code

Q3: How are handlers created in child threads?

This is a common problem when we use Handler in an Activity. If we use an anonymous inner class to inherit Handler, we use a method that does not write any constructors (this constructor is deprecated by Android Studio, and it is best to specify Looper to avoid errors). Gets the Looper of the current thread. If you are in an Activity, the Activity’s Looper already prepares and loops in ActivityThread#main. So prepare and loop are also required in child threads.

Thread {// Case#1 // Run on mainLooper val handlerMain = object: Handler(mainLooper){ override fun handleMessage(msg: Message) { when(msg.what){ TEST_HANDLER -> Log.d("TEST Main", "Notification received ") else -> {}}}} // Case#2 run on thread-specified Looper looper.prepare () val handlerOther = object: Handler(Looper.myLooper()!!) { override fun handleMessage(msg: Message) { when(msg.what){ TEST_HANDLER -> Log.d("TEST other", "Notification") else - > {}}}} handlerOther. SendEmptyMessage (TEST_HANDLER) stars. Loop () stars. MyLooper ()!!!!! .quitSafely() } }Copy the code

Also remember to exit Looper at this point, because the Looper#loop thread is responsible for the message loop and uses for(;). Keep running and block when there is no message. Here, use quit to exit the for loop, freeing resources associated with the child thread.

Q4: Why does the loop for loop not cause ANR?

A4: First, take a look at the definition of ANR on the official website

Although the loop in Looper is an infinite loop, recall Message#next, which blocks when the thread has not received the message, the message queue will still process if the user continues to have time to input:

for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } / /... }Copy the code

In summary, the for loop in Looper#loop does not cause the main thread to fail to process messages, but rather the Handler’s messaging mechanism requires the for loop to continually try to retrieve messages from the message queue.

Reference data

  • The role of nativePollOnce
  • Handler message barrier you should know
  • Handler Memory leak details and solution
  • How many classic interview questions can you answer?