Code analysis Android message mechanism

  • 2021-08-28 21:05:19
  • OfStack

We know that when programming, many operations (such as updating UI) need to be done in the main thread, and time-consuming operations (such as network connection) need to be placed in child threads, otherwise ANR will be caused. Therefore, we often use Handler to realize message transmission between threads, and this is the operation mechanism of Handler.

The operation of Handler is mainly supported by two classes: Looper and MessageQueue. Friends familiar with development know that Handler cannot be created in child threads by default, because there is no message queue in child threads. When you need to create an Handler bound to a child thread, the standard code is as follows:


class LooperThread extends Thread {
  public Handler mHandler;
  public void run() {
    Looper.prepare();
    mHandler = new Handler() {
      public void handleMessage(Message msg) {
        // process incoming messages here
      }
    };
    Looper.loop();
   }
}

Before you create an Handler, you need to call the Looper. prepare () method and then the Looper. loop () method. That is to say, the functional implementation of Handler is based on Looper.


static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
final MessageQueue mQueue;
final Thread mThread;

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

Since the message loop of Looper is an infinite loop, and a thread can only have at most one Looper, the Looper. prepare () function first checks whether the thread already has one Looper, and throws an exception if so. Looper stores independent Looper instances for each thread through ThreadLocal class. Simply talk about the implementation principle of ThreadLocal under 1:

Java concurrent programming: An in-depth analysis of ThreadLocal

First, within each thread Thread, there is a member variable threadLocals of type ThreadLocal. This threadLocals is used to store the actual variable copy, with the key value being the current ThreadLocal variable and value being the variable copy.

Initially, in Thread, threadLocals is empty. When get () method or set () method is called through ThreadLocal variable, threadLocals in Thread class will be initialized, and the current ThreadLocal variable will be taken as the key value, and the copy variable to be saved by ThreadLocal will be taken as value, which will be stored in threadLocals.

Then, in the current thread, if you want to use copy variables, you can look them up in threadLocals through get 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;
  for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
      // No message indicates that the message queue is quitting.
      return;
    }
    msg.target.dispatchMessage(msg);
    msg.recycleUnchecked();
  }
}

You can see a familiar exception message in Line 4, indicating that there is no Looper associated with the current thread, so message delivery cannot be carried out. The Looper. loop () method itself is an infinite loop, constantly taking out Message objects in MessageQueue for processing, and then calling Message. recycleUnchecked () method to recycle them, which is why it is officially recommended to use Message. obtain () method to obtain Message instances instead of directly creating new objects. When there is no message to process, the MessageQueue. next () method blocks until a new message arrives.

For MessageQueue, we only need to focus on two functions, one is MessageQueue. enqueueMessage () and the other is MessageQueue. next (), which correspond to the insert and fetch operations of the queue, respectively. Queues in MessageQueue are implemented using a single linked list, with the Message. next attribute pointing to its next one element.


boolean enqueueMessage(Message msg, long when) {
  synchronized (this) {
    msg.markInUse();
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    if (p == null || when == 0 || when < p.when) {
      msg.next = p;
      mMessages = msg;
    } else {
      Message prev;
      for (;;) {
        prev = p;
        p = p.next;
        if (p == null || when < p.when) {
          break;
        }
      }
      msg.next = p; // invariant: p == prev.next
      prev.next = msg;
    }
  }
  return true;
}

When inserting elements into MessageQueue, the insertion position needs to be determined according to the size of Message. when attribute, which represents the time that Meesage needs to be processed. Take Handler. sendMessage () function as an example.


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) {
  msg.target = this;
  if (mAsynchronous) {
    msg.setAsynchronous(true);
  }
  return queue.enqueueMessage(msg, uptimeMillis);
}

From the call flow point of view, the Handler. sendMessage () function actually inserts an element with the Message. when attribute as the current time into the message queue of MessageQueue.

For the MessageQueue. next () function, its simple function is to fetch the element in the header of MessageQueue and then execute the Handler. dispatchMessage () function.


public void dispatchMessage(Message msg) {
  if (msg.callback != null) {
    handleCallback(msg);
  } else {
    handleMessage(msg);
  }
}

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

If we send an Runnable object using the Handler. post () function, the final Runnable object will be executed in the Handler. handleCallback () function. If it is a normal Message, it is distributed to a familiar function, Handler. handleMessage (), which is why we all need to override this function to process messages.


Related articles: