Android basic advanced-message mechanism

Android basic advanced-message mechanism

table of Contents

  1. Android message mechanism process
  2. Handler
  3. Message
  4. MessageQueue
  5. Looper
  6. HandleThread
  7. data
  8. reward

Digression

In addition to the "Audio and Video Development Journey Series", I want to take time to learn and strengthen my relatively weak Java&Android foundation, which is more in line with my inner pursuit and self-expectation. Begin another learning journey in parallel, starting with the Handler message mechanism, combining the flow of the message mechanism and the source code for learning and analysis.

1. Android message mechanism process

We first use the following two pictures to understand the Android message mechanism process and the relationship between the key classes, and then we will analyze the source code one by one.

The flow of the message mechanism

The relationship between Handler, Message, MessageQueue, Looper

Picture source-Android message mechanism 1-Handler (Java layer)

2. Handler

Handler has two main purposes:

  1. Schedule messages to be executed at a certain point in time;
  2. Communication between different threads

2.1 Global variables

final Looper mLooper;//There is a reference to Looper final MessageQueue mQueue;//There is a reference to MessageQueue final Callback mCallback; final boolean mAsynchronous; IMessenger mMessenger; Copy code

2.2 Construction method

public Handler() { this(null, false); } public Handler(@NonNull Looper looper) { this(looper, null, false); } public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) { mLooper = looper; mQueue = looper.mQueue; mCallback = callback; mAsynchronous = async; } Copy code

2.3 Get Message

//Get a Message from the Message reuse pool public final Message obtainMessage() { return Message.obtain(this); } //It is basically the same as the above method, the difference is that after obtaining the Message from the reuse pool, assign a value to what public final Message obtainMessage(int what) { return Message.obtain(this, what); } //...Other obtainMessage is similar Copy code

2.4 Sending a message

Picture source-Android message mechanism 1-Handler (Java layer)

Let's pick a few sending methods to see

** sendMessage: Send a Message, when is the current time **
MessageQueue matches and inserts the position according to when

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); } Copy code

** post: Get Message from the message reuse pool and set the Callback of Message**

public final boolean post(Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); } private static Message getPostMessage(Runnable r) { Message m = Message.obtain(); m.callback = r; return m; } Copy code

** postAtFrontOfQueue(): Insert the message into the head of the queue **
Add a message with when is 0 to the queue by calling sendMessageAtFrontOfQueue, that is, insert it into the head of the queue. Note that MessageQueue#enqueueMessage is inserted into the linked list According to the when comparison (when <p.when), if there have been multiple messages with when equal to 0 in the queue before, this new one will be added to the previous when when is also 0.

public final boolean postAtFrontOfQueue(Runnable r) { return sendMessageAtFrontOfQueue(getPostMessage(r)); } public final boolean sendMessageAtFrontOfQueue(Message msg) { MessageQueue queue = mQueue; ...... //The third parameter is 0, that is, the when of Message is 0, and it is inserted into the head of the queue. Note that when MessageQueue#enqueueMessage is inserted into the linked list, it is compared according to when (when <p.when), if it has been before There are multiple messages with when equal to 0 in the queue, and this new one will be added to the back of the previous when when is also 0. return enqueueMessage(queue, msg, 0); } Copy code

2.5 dispatch message dispatchMessage

The priority is as follows:
Message callback method callback.run()>
Handler callback method mCallback.handleMessage(msg)> Handler default method handleMessage(msg)

public void dispatchMessage(@NonNull Message msg) { //Message callback method, the highest priority if (msg.callback != null) { handleCallback(msg); } else { //Handler's mCallBack priority is second if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } //Handler's handleMessage method has the lowest priority (most of them implement Message processing in this method) handleMessage(msg); } } Copy code

3. Message

Global variable

//Some important variables public int arg1; public int arg2; public Object obj; public long when; Bundle data; Handler target;//There is a reference to Handler in Message Runnable callback; //Message has a next pointer, which can form a singly linked list Message next; public static final Object sPoolSync = new Object(); //The first Message in the reuse pool private static Message sPool; //The size of the reuse pool, the default maximum is 50 (what happens if there are more than the maximum number of messages in the reuse pool in a short time, renew) private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 50; Copy code

Construction method
Check whether there is a message that can be reused. If there is, the number of messages that can be reused in the reuse pool is reduced by one, and the message is returned; if there is no new one. Note that the default maximum number of reuse pools is 50.

public static Message obtain() { synchronized (sPoolSync) { //Check if there is a message that can be reused if (sPool != null) { //Remove the first Message Message m = sPool; sPool = m.next; m.next = null; m.flags = 0;//clear in-use flag //The number of reusable Messages in the reuse pool minus one sPoolSize--; return m; } } //If there is no Message in the reuse pool, renew return new Message(); } Copy code

**recycleUnchecked**
//Asynchronous message when marking a Message. Normally, it is a synchronous message. When a synchronous barrier is encountered, the first asynchronous message is executed first. Regarding synchronization barriers, we will introduce methods such as next in MessageQueue.

public void setAsynchronous(boolean async) { if (async) { flags |= FLAG_ASYNCHRONOUS; } else { flags &= ~FLAG_ASYNCHRONOUS; } } 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) { //The number of messages that can be reused is 50, if it exceeds, it will not be reused if (sPoolSize <MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } } Copy code

//toString and dumpDebug can dump out the message information, and can help analyze
android.os.Message#toString(long) when encountering some problems

android.os.Message#dumpDebug

4. MessageQueue

MessageQueue is a singly linked list priority queue.
Message cannot be added directly to MessageQueue. It must be added through Handler and the corresponding Looper.

variable

//The first Message in the MessageQueue linked list Message mMessages; Copy code

** next: Take the next message to be executed from the message queue **
If it is a synchronous barrier message, find the first asynchronous message in the first queue.
If the execution time of the first Message is later than the current time , Record how long it will take to execute; otherwise, find the next Message to be executed.
The loop method of Looper later will call this method from queue.next to get the next Message that needs to be executed, which will call the blocking native method nativePollOnce, which is used to "wait" until the next message is available. If The time spent during this call is very long, indicating that the corresponding thread has no actual work to do, and there will be no ANR because of this. ANR has no relationship with this.

The key code is as follows:

Message next() { //Pointer to the native layer MessageQueue final long ptr = mPtr; if (ptr == 0) { return null; } ...... for (;;) { //Blocking operation, when waiting for nextPollTimeoutMillis, or the message queue is awakened //nativePollOnce is used to "wait" until the next message is available. If it takes a long time during this call, it indicates that the corresponding thread has no actual work to do, so there will be no ANR, ANR and this are not half a dime relationship. nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { final long now = SystemClock.uptimeMillis(); Message prevMsg = null; //Create a new Message pointing to the head of the current message queue Message msg = mMessages; //If it is a synchronous barrier message, find the first asynchronous message in the first queue if (msg != null && msg.target == null) { do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { //If the execution time of the first Message is later than the current time, how long will it take to record the execution if (now <msg.when) { nextPollTimeoutMillis = (int) Math.min(msg.when-now, Integer.MAX_VALUE); } else { //Otherwise, take the current Message from the linked list, and point next to the next Message in the linked list mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } //Remove the value of the current Message, set next to empty msg.next = null; msg.markInUse(); return msg; } } ..... //android.os.MessageQueue#mQuitting is true when quit //If you need to exit, execute it immediately and return a null Message, android.os.Looper.loop exits the Looper loop after receiving a null message if (mQuitting) { dispose(); return null; } ...... if (pendingIdleHandlerCount <= 0) { //Note that if there is no message to be executed, mBlocked is marked as true, and enqueueMessage will determine whether to call nativeWake to wake up based on this mark mBlocked = true; continue; } ...... } ...... } Copy code

enqueueMessage: Insert a Message into the message queue

If the message list is empty, or the inserted Message is executed earlier than the first message in the message list, insert
it directly into the head, otherwise it will find a suitable position in the list. Normally, there is no need to wake up the event queue. The following two situations except:

  1. There is only the message just inserted in the message list, and mBlocked is true, that is, it is in the blocking state, and it will wake up after receiving a message.
  2. The head of the linked list is a synchronization barrier, and the message is the first asynchronous message

Who to wake up? NativePollOnce blocked in MessageQueue.next

The specific implementation is as follows,

On how to find the right location? This involves the insertion algorithm of the linked list: introduce a prev variable that points to p and message (if it is the first execution inside the for loop), and then move p to next, and compare it with the Message that needs to be inserted when

The key code is as follows:

boolean enqueueMessage(Message msg, long when) { ...... synchronized (this) { msg.markInUse(); msg.when = when; Message p = mMessages; boolean needWake; //If the message list is empty, or the inserted Message is executed earlier than the first message in the message list, it is directly inserted into the header if (p == null || when == 0 || when <p.when) { msg.next = p; mMessages = msg; needWake = mBlocked; } else { //Otherwise find a suitable position in the linked list to insert //Normally, there is no need to wake up the event queue, unless the head of the linked list is a synchronization barrier, and the message is the first asynchronous message needWake = mBlocked && p.target == null && msg.isAsynchronous(); //The specific implementation is as follows, this is a picture to illustrate //The linked list introduces a prev variable, which points to p and message (if it is the first execution inside the for loop), and then moves p to next and compares it with the Message that needs to be inserted when Message prev; for (;;) { prev = p; p = p.next; if (p == null || when <p.when) { break; } if (needWake && p.isAsynchronous()) { needWake = false; } } msg.next = p; prev.next = msg; } //If the inserted message is an asynchronous message, and the first message in the message list is a synchronous barrier message. //Or there is only the message just inserted in the message list, and mBlocked is true, that is, it is in a blocking state, and wakes up after receiving a message Who to wake up? NativePollOnce blocked in MessageQueue.next if (needWake) { nativeWake(mPtr); } } return true; } Copy code

Simply take a look at the native epoll (this piece has not been analyzed in depth yet, let's add it in a later chapter)

nativePollOnce and nativeWake use the epoll system call, which can monitor the IO events in the file descriptor. nativePollOnce is called on a file descriptor

epoll_wait
, And nativeWake writes an IO operation to the descriptor
epoll belongs to the IO multiplexing mode call, call
epoll_wait
Wait. Then the kernel removes the epoll waiting thread from the waiting state, and the thread continues to process new messages

** removeMessages: Remove the corresponding message in the message list ** It
should be noted that the implementation of this function is divided into the removal of the head meg and the removal of the non-head msg.
Remove the head of the message list and need to remove the same msg
eg: msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; Need to remove msg with what is 0, I.e. remove the first three

Remove the non-head corresponding messages in the message list, eg: msg1.what=1; msg2.what=0; msg3.what=0; You need to remove the message with what is 0, that is, remove the subsequent messages, everywhere Reflect the query and removal algorithm of the linked list

The key code is as follows:

void removeMessages(Handler h, int what, Object object) { ...... synchronized (this) { Message p = mMessages; //Remove the header in the message list and need to remove the same msg eg: msg1.what=0;msg2.what=0;msg3.what=0; msg4.what=1; Need to remove what is 0 msg, remove the first three while (p != null && p.target == h && p.what == what && (object == null || p.obj == object)) { Message n = p.next; mMessages = n; p.recycleUnchecked(); p = n; } //Remove the corresponding message that is not the head in the message list, eg:msg1.what=1;msg2.what=0;msg3.what=0; Need to remove the message with what is 0, that is, remove subsequent messages , Everywhere reflects the query and removal algorithm of the linked list while (p != null) { Message n = p.next; if (n != null) { if (n.target == h && n.what == what && (object == null || n.obj == object)) { Message nn = n.next; n.recycleUnchecked(); p.next = nn; continue; } } p = n; } } } Copy code

** postSyncBarrier: send synchronization barrier message **

The synchronization barrier is also a message, but the target of this Message is null. Send the synchronization barrier message through ViewRootImpl#scheduleTraversals()
. The insertion position of the synchronization barrier message is not all at the head of the message list, but based on when and other information. : If when is not 0, and the message list is not empty, find the position where the synchronization barrier should be inserted in the message list; if prev is empty, the synchronization message is inserted into the head of the queue.

The key code is as follows:

/** * android.view.ViewRootImpl#scheduleTraversals() send synchronization barrier message * @param when * @return */ private int postSyncBarrier(long when) { //Enqueue a new sync barrier token. //We don't need to wake the queue because the purpose of a barrier is to stall it. synchronized (this) { final int token = mNextBarrierToken++; //Synchronization barrier is also a message, but the target of this Message is null final Message msg = Message.obtain(); msg.markInUse(); msg.when = when; msg.arg1 = token; Message prev = null; Message p = mMessages; if (when != 0) { //If when is not 0, the message list is not empty, find the position where the synchronization barrier should be inserted in the message list while (p != null && p.when <= when) { prev = p; p = p.next; } } if (prev != null) {//invariant: p == prev.next msg.next = p; prev.next = msg; } else { //If prev is empty, the synchronization message is inserted into the head of the queue msg.next = p; mMessages = msg; } return token; } } Copy code

dump: MessageQueue information
Sometimes we need to dump out the current looper's Message information to analyze some problems. If there are too many messages in the Queue, if there are too many messages, it will affect the execution of the messages in the queue, which may result in slower logic processing. It may even lead to situations such as ANR. The default multiplexing pool of MessageQueue is 50. If too many messages are queued, performance will also be affected. Through dump Message information can help analysis.

mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);

void dump(Printer pw, String prefix, Handler h) { synchronized (this) { long now = SystemClock.uptimeMillis(); int n = 0; for (Message msg = mMessages; msg != null; msg = msg.next) { if (h == null || h == msg.target) { pw.println(prefix + "Message "+ n + ":" + msg.toString(now)); } n++; } pw.println(prefix + "(Total messages: "+ n + ", polling=" + isPollingLocked() + ", quitting=" + mQuitting + ")"); } } Copy code

5. Looper

Looper mainly involves several important methods of construction, prepare, and loop. In the design to ensure that one thread has and only one Looper, it adopts ThreadLocal and code logic control.

variable

//Some important variables static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); final MessageQueue mQueue; final Thread mThread; Copy code

Construction method
Create a MessageQueue that corresponds to Looper one-to-one when constructing Looper

private Looper(boolean quitAllowed) { //New one-to-one corresponding MessageQueue when Looper is constructed mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); } Copy code

prepare
Here we can see how the message is to ensure that a mechanism is only one thread Looper.

//The quitAllowed parameter allows quit, the Looper of the UI thread is not allowed to quit, and the others are allowed to quit private static void prepare(boolean quitAllowed) { //Ensure that a thread can only have one Looper, here sThreadLocal if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } Copy code

loop
We have analyzed in the next method of MessageQueue that nativePollOnce may block until the message is obtained.
If next returns a null Message, exit the Looper loop, otherwise send msg.
After the removed msg is executed, it will be added to the recycling pool to wait for reuse. We have also analyzed recycleUnchecked in Message. You can look back if you are unclear.

public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } ... for (;;) { //The next method is a blocking method. The next method of MessageQueue has previously analyzed that nativePollOnce may block until the message is obtained. Message msg = queue.next(); //Receive empty msg, loop exit. So when will you receive an empty msg? quit if (msg == null) { //No message indicates that the message queue is quitting. return; } //msg dispatch, msg.target is the Handler, which calls the dispatchMessage of the Handler to dispatch the message msg.target.dispatchMessage(msg); ... //msg recycling msg.recycleUnchecked(); } Copy code

6. HandleThread

HandlerThread is a Thread with Looper.

Global variable

public class HandlerThread extends Thread { int mPriority;//Thread priority int mTid = -1;//thread id Looper mLooper; private Handler mHandler; ...... } Copy code

Construction method

public HandlerThread(String name) { super(name); //Used to set the thread priority during run Process.setThreadPriority(mPriority); mPriority = Process.THREAD_PRIORITY_DEFAULT; } public HandlerThread(String name, int priority) { super(name); mPriority = priority; } Copy code

run method
Carry out Looper's prepare and loop call, configure Looper environment

@Override public void run() { //Thread id mTid = Process.myTid(); //Call Looper's prepare method to add the only Looper currently associated with the thread to sThreadLocal Looper.prepare(); synchronized (this) { //Get Looper from sThreadLocal mLooper = Looper.myLooper(); notifyAll(); } //Set the thread priority, the default THREAD_PRIORITY_DEFAULT, if it is a background business, it can be configured as THREAD_PRIORITY_BACKGROUND, set according to specific scenarios Process.setThreadPriority(mPriority); //You can do some preset operations onLooperPrepared(); //Start looper loop Looper.loop(); mTid = -1; } Copy code

The general process of using HandlerThread is as follows

//Step 1: Create and start the HandlerThread thread, which contains Looper HandlerThread handlerThread = new HandlerThread("xxx"); handlerThread.start(); //Step 2: Create Handler Handler handler = new Handler(handlerThread.getLooper()); handler.sendMessage(msg); Copy code

This has a drawback, that is, every time you use Handler, you must new HandlerThread, and Thread takes up more memory,
can you reduce the creation of Thread, or the reuse of Thread.
And realize that Message can be executed in time and not be queued The previous Message is blocked;
this is indeed a very interesting and challenging thing.

7. Information

  1. Android source code
  2. ThreadLocal principle analysis and usage scenarios
  3. Android message mechanism 1-Handler (Java layer)
  4. Android's comprehensive analysis of the Handler mechanism (final article): summary of common problems
  5. Does Handler really understand?
  6. Android synchronization barrier? Block wake up? Secrets hidden in Handler
  7. Epoll summary of IO multiplexing
  8. Are you aware of these 15 questions about Handler?
  9. "Detailed source code" Android system blood: Handler
  10. Confident, this is the best ThreadLocal analysis
  11. Android Handler: Take you in-depth analysis of the source code of the Handler mechanism
  12. Android Handler.removeMessage removes all postDelayed issues
  13. NativePollOnce of MessageQueue in Android

8. Harvest

Through the analysis of the source code of the Android message mechanism, it is clear

  1. The flow of the Android message mechanism;
  2. How to ensure that a thread corresponds to a Looper
  3. The data structure design of the singly linked list of Message and the reuse pool mechanism of Message
  4. MessageQueue then adds and takes out the implementation of Message, as well as blocking wake-up and synchronization barriers.

The analysis on ThreadLocal and native layer has not been thoroughly understood yet, let s analyze and learn the next two articles.

Thank you for reading the
next article on our application in ThreadLocal and its Android messaging mechanism. Welcome to follow the official account "Audio and Video Development Journey" to learn and grow together.
Welcome to exchange