Analysis of message mechanism of Android from source code point of view

  • 2021-12-11 19:07:44
  • OfStack

Preface to the table of contents How ThreadLocal works How Looper works How Handler works Summarize References

Preface

When it comes to the message mechanism of Android, it mainly refers to the operation mechanism of Handler. It includes the working process of MessageQueue and Looper.

Before starting the text, throw out two questions:

Why should the update of UI be done in the main thread? Why isn't the main thread stuck in Android because of the dead loop in Looper. loop ()?

The decision of the UI thread is done in the checkThread method in ViewRootImpl.

For question 1, here is a simple answer:

If UI can be modified in sub-threads, the concurrent access of multi-threads may lead to the unpredictability of UI controls. Lock will reduce the access efficiency of UI and block the execution of other threads. Therefore, the simplest and most effective method is to use single-threaded model to handle UI operations.

The operation of Handler can not be separated from the support of the underlying MessageQueue and Looper. MessageQueue is translated as a message queue, which stores Message needed by Handler. MessageQueue is not a queue, but actually uses the data structure of a single linked list to store Message.

So how does Handler get Message? At this time, Looper is needed. Looper starts an infinite loop through Looper. loop (), and constantly takes messages from MessageQueue and passes them to Handler.

There is another knowledge point here is the acquisition of Looper, and it is necessary to improve a storage class here: ThreadLocal

Working Principle of ThreadLocal

ThreadLocal is a data storage class inside a thread, which can store the data in a certain thread, but the data of this thread cannot be obtained by other threads. Let's see if this view is correct by 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();
 }

It can be seen that its set and get methods are the operations done in the current thread, and inside ThreadLocalMap is an array table. This ensures that the data in different threads does not interfere with each other.

In addition to using Looper obtained in Handler, ThreadLocal is also used for some complex scenarios, such as listener delivery.

We have a brief understanding of ThreadLocal, so let's sort out the message mechanism step by step from New Handler ().

Working Principle of Looper


// Handler.java

 public Handler() {
  this(null, false);
 }
 // callback  Message callback; async  Synchronize or not 
 public Handler(Callback callback, boolean async) {
  ...
  // 1.  Obtain first looper
  mLooper = Looper.myLooper();
  if (mLooper == null) {
   throw new RuntimeException(
    "Can't create handler inside thread " + Thread.currentThread()
      + " that has not called Looper.prepare()");
  }
  // 2.  Get MessggeQueue
  mQueue = mLooper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
 }

We usually use the parameterless method, which passes in an empty callback and false.


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

The ThreadLoacal class we mentioned before appears here, so when was the looper value set?

Its setting method is actually in prepare method and prepareMainLooper method. Let's look at it respectively:


 public static void prepare() {
  prepare(true);
 }

 private static void prepare(boolean quitAllowed) {
  //  In creating looper Before, judging looper Is it related to threadloacal Bound, so is this prepare You can only set 1 The reason for all times. 
  if (sThreadLocal.get() != null) {
   throw new RuntimeException("Only one Looper may be created per thread");
  }
  sThreadLocal.set(new Looper(quitAllowed));
 }

 public static void prepareMainLooper() {
  //  In fact, it is still called here prepare Method 
  prepare(false);
  synchronized (Looper.class) {
   if (sMainLooper != null) {
    throw new IllegalStateException("The main Looper has already been prepared.");
   }
   sMainLooper = myLooper();
  }
 }

The prepare method can only be set once through the above, so why can we use it directly in the main thread? The entry to the app program is in the main method in ActivityThread:


public static void main(String[] args) {
  ...

  //1.  Initialization Looper Object 
  Looper.prepareMainLooper();
  
  // 2.  Open an infinite loop 
  Looper.loop();
  throw new RuntimeException("Main thread loop unexpectedly exited");
 }

See, the initialization is here, so let's look at the initialization method of looper again:


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

The initialization of Looper does two things: creating the message queue MessageQueue and getting the current thread. Here, we can get a conclusion:

The prepare method can only be called once in one thread. The initialization of Looper can only be called once in one thread. Finally, we can know that one thread corresponds to one Looper, and one Looper corresponds to one MessageQueue.

Looper can be understood as a factory line, and Message is continuously taken from MessageQueue. The way to open the factory line is Looper. loop ()


 public static void loop() {
  final Looper me = myLooper();
  // 1.  Judge looper Does it exist 
  if (me == null) {
   throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
  }
  final MessageQueue queue = me.mQueue;
  ...
  
  //2.  Open 1 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);
    }
   }
  ...
  }
 }

The looper method continuously takes Message messages from MessageQueue by opening an infinite loop. When message is empty, it exits the loop, otherwise it calls msg. target. dispatchMessage (msg) method, and target is the Handler object bound by msg.

Working Principle of Handler

Ok, here we go back to 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);
  }
 }

This handleMessage is the method we need to implement. So how is Handler set up in Message? Let's look at the well-known 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) {
  //  Here comes the key code! 
  msg.target = this;
  if (mAsynchronous) {
   msg.setAsynchronous(true);
  }
  return queue.enqueueMessage(msg, uptimeMillis);
 }

As you can see, handler is assigned to target of msg in enqueueMessage through a series of 1 methods. The last call is in the enqueueMessage method of MessageQueue:


 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;
 }

The enqueueMessage method mainly does two things:

First, it is judged whether handler exists and is in use. Then insert it into MessageQueue in chronological order.

By this point, the basic process has been sorted out. Back to our original question: Looper. loop () is an infinite loop, why won't it block the main thread?

Let's look at the next method of MessageQueue:


// Handler.java

 public Handler() {
  this(null, false);
 }
 // callback  Message callback; async  Synchronize or not 
 public Handler(Callback callback, boolean async) {
  ...
  // 1.  Obtain first looper
  mLooper = Looper.myLooper();
  if (mLooper == null) {
   throw new RuntimeException(
    "Can't create handler inside thread " + Thread.currentThread()
      + " that has not called Looper.prepare()");
  }
  // 2.  Get MessggeQueue
  mQueue = mLooper.mQueue;
  mCallback = callback;
  mAsynchronous = async;
 }
0

nativePollOnce method is an native method. When this native method is called, the main thread will release CPU resources and enter the sleep state until the next message arrives or a transaction occurs. The main thread will wake up by writing data to the write end of pipe pipeline. The epoll mechanism is adopted here. For detailed analysis of nativePollOnce, please refer to nativePollOnce function analysis

Summarize

The app program starts from the main method in ActivityThread, and creates Looper and MessageQueue and binds ThreadLocal to threads through Looper. prepare (). When we create Handler, we use ThreadLocal to get Looper in this thread and MessageQueue bound on Looper. Bind msg to Handler through the Handler. sendMessage () method, and then insert msg into MessageQueue in chronological order. After the main thread is created, Looper. loop () starts a (no resource) dead loop, continuously extracts Message from MessageQueue that already exists in Looper, then calls dispatchMessage (msg) method of Handler bound to Message that is not empty, and finally calls handlerMessage method that we replicate.

References

Exploration of Androi Development Art

The above is from the source point of view of Android message mechanism details, more information about Android message mechanism please pay attention to other related articles on this site!


Related articles: