In this article, we’ll discuss Android’s messaging mechanisms. Speaking of handlers, those of you who have had some Experience with Android development should know what they do. They are usually used to notify the main thread to update the UI. But handlers need the underlying MessageQueue and Looper to work. In this article, we will discuss the relationship between the three of them and how they are implemented.
In this article, you can’t avoid ThreadLocal because you’re dealing with threading. I’ve analyzed this API in a previous article, and you can learn more about how it works and how it works in this article, instead of focusing on Java Concurrent Programming: Using ThreadLocal and Its Source code Implementation.
1, Handler function
In general, we use Handler to update the UI in the main thread after we do asynchronous operations in the non-main thread. The reason for this is that views in Android are not thread-safe. Views are designed to be non-thread-safe because: 1). Locking a View increases the complexity of using the control. 2). Locking will reduce the efficiency of control execution. But Handler isn’t just used to update the UI in the main thread. It actually has two functions:
- Task scheduling: Yes
post()
和send()
To specify a task to be executed at a certain time; - Thread switching: You’ve probably used RxJava before, but if you’re using it on Android, you’ll need to work with RxAndroid, and RxAndroid uses handlers internally for thread switching.
In the following, we will take a look at the role and principle of these two functions respectively.
1.1 Task Scheduling
Using Hanlder allows a task to be executed at a certain point in time or wait for a certain period of time to be executed. Handler provides a number of methods for this purpose, which we can classify by naming into post() and sned() methods. The post() class specifies a Runnable to execute at a certain point in time, and the send() class specifies a Message to execute at a certain point in time.
Message is a class defined in Android. It has several internal fields, such as What, arg1, arg2, replyTo, and sendingUid, to help us specify the content and object of the message. Message also implements the Parcelable interface, indicating that it can be used for cross-process transport. In addition, it internally defines a next field of type Message, which indicates that Message can be used as a linked list node. In fact, MessageQueue contains only one mMessage, which is the head node of the linked list. Therefore, the Message queue inside MessageQueue is essentially a single linked list, and the node of each linked list is Message.
When a method of type POST () is called to schedule a Runnable, it is first wrapped as a Message, and then the task is distributed using methods of the send() class. So, both the post() and send() methods will eventually be queued using the Handler’s sendMessageAtTime() method:
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
/ /... Has nothing to do with the code
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
Copy the code
Scheduling tasks using Handler is very simple. The following code implements the logic to have a Runnable execute after 500ms:
new Handler().postDelayed(new Runnable() {
@Override
public void run(a) {
// do something}},500);
Copy the code
The above execution mode will not cause any problems in the main thread, but may cause exceptions if you execute it in a non-main thread. We’ll talk about why later.
Since each Runnable is also wrapped as a Message after being sent by post(), what’s the point of Message?
The Runnable wrapped procedure relies on the getPostMessage() method inside the Handler. Here is the definition of the method:
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
Copy the code
As you can see, our Runnable will be assigned to the Message callback. Messages of this type cannot be processed in more detail. That is, we cannot take advantage of the what, arg1, and other fields of the message (which we did not set ourselves). If we want to use these fields of Message, we need:
- First, use
send()
Type to pass our Message to Handler; - Then, our Handler has to override
handleMessage()
Method, in which each Message is taken and processed in turn based on the information inside it.
Here is an example of a method of the send() type. First, we define a Handler and override its handleMessage() method to process the message:
private final static int SAY_HELLO = 1;
private static Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SAY_HELLO:
LogUtils.d("Hello!");
break; }}};Copy the code
We then send a message to this Handler:
Message message = Message.obtain(handler);
message.what = SAY_HELLO;
message.sendToTarget();
Copy the code
Thus, our Handler receives the message and tells it to say “SAY_HELLO” based on its “What” and prints out the log message. In addition to calling the sendToTarget() method of Message, we can also call the handler’s sendMessage() method directly (sendToTarget() calls the handler’s sendMessage() internally).
1.2 Thread Switching
Here we use a sample code that instantiates a Handler in the main thread, and then in a method we start a thread and execute a task. After 2 seconds, the task is over, so let’s update the UI.
// Retrieve Handler from the main thread
private static Handler handler = new Handler();
// Update the UI to send messages to the main thread
new Thread(() -> {
try {
Thread.sleep(2000);
handler.post(() -> getBinding().tv.setText("Main thread updates UI"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Copy the code
We were able to update the UI in the main thread because our Handler fetched it in the main thread. Then, after we call the handler’s post() method, the Runnable passed in is wrapped as a Message and added to the main thread’s Message queue, where it is picked up and executed by the main thread’s corresponding Looper. As a result, the operation of the Runnable ends up in the main thread.
You might find it difficult to get a Handler in the main thread and then use it. Don’t worry, we have another way to solve this problem. We can directly use Looper’s getMainLooper() method to get the Looper corresponding to the main thread, and then use it to instantiate a Handler and use that Handler to process messages:
new Handler(Looper.getMainLooper())
.post(() -> getBinding().tv.setText("Main thread updates UI"));
Copy the code
Essentially, when we call the Handler’s parameterless constructor, or when we don’t specify Looper, we instantiate the Handler directly using the Looper of the current thread. The Looper for each thread is stored in its local variable ThreadLocal. An exception is thrown when there is no Looper in a thread’s local variable. So, this is why we said that when we instantiate a Handler directly with new, we might get an error.
The Looper corresponding to the main thread is created in ActivityThread’s static method main(), which calls Looper’s prepareMainLooper() static method to create the Looper corresponding to the main thread. Looper’s static loop() method is then called to open the Looper loop to continuously process messages. The ActivityThread here is used to handle activity and broadcast requests in the application process and is called when the application starts. ActivityThread internally defines an inner class H that inherits from Handler and runs in the main thread to handle the requests received from various activities, broadcasts, and services.
In addition to using the Looper corresponding to the main thread, we can also enable our own thread Looper. In the following code, we start a thread and call Looper’s prepare() static method in the thread. Looper creates Looper for our current thread and then adds it to the local variable of the current thread. We then open the Looper loop when we call Looper’s loop() method to continuously process the message:
new Thread(() -> {
LogUtils.d("+ + + + + + + + +" + Thread.currentThread());
Looper.prepare();
new Handler().post(() -> LogUtils.d("+ + + + + + + + +" + Thread.currentThread()));
Looper.loop();
}).start();
Copy the code
From the above content, we can see that the Handler is able to achieve thread switching, the main reason is that its internal message queue is corresponding to each thread. The sent task is executed in the message queue corresponding to the thread. Success in obtaining the message queue for that thread relies on ThreadLocal to store the message queue for each thread.
2, source code parsing
Above, we have analyzed the two main uses of handlers, and in the process, we have covered a lot of the underlying design of handlers, MessageQueue, and Looper. In the above article, we only used words to describe. In this article, let’s verify some of the things we mentioned above with the source code.
2.1 Instantiating the Handler
Handler provides multiple overloaded constructors, which we can divide into two main types. One requires an explicit Looper in the constructor, and the other does not need to specify any Looper in the constructor. In the constructor, the Looper corresponding to the current thread is obtained to initialize the Handler.
The first method of initialization will eventually call the following method to complete the initialization. This is a simple, basic assignment operation:
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Copy the code
The second method of initialization ends up calling the following method. The static method of Looper, myLooper(), is used to get the Looper corresponding to the current thread. An exception is thrown if the current thread does not have any Looper.
public Handler(Callback callback, boolean async) {
// Check for potential memory leaks
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && (klass.getModifiers() & Modifier.STATIC) = =0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: "+ klass.getCanonicalName()); }}// Use Looper's static method myLooper() to get Looper for the current thread
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException();
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Copy the code
The static Looper method myLooper() uses the thread-local variable sThreadLocal to retrieve the Looper previously stored inside the thread:
public static @Nullable Looper myLooper(a) {
return sThreadLocal.get();
}
Copy the code
2.2 Looper initialization
We also talked about the Looper creation process earlier. Looper for the main thread is called in the ActivityThread’s main() method:
public static void main(String[] args) {
/ /... Has nothing to do with the code
Looper.prepareMainLooper();
/ /... Has nothing to do with the code
// Open Looper loop
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
Copy the code
PrepareMainLooper () ¶ Here, Looper’s static method prepareMainLooper() is called to initialize Looper for the main thread:
public static void prepareMainLooper(a) {
prepare(false);
synchronized (Looper.class) {
if(sMainLooper ! =null) {
throw new IllegalStateException("The main Looper has already been prepared."); } sMainLooper = myLooper(); }}Copy the code
Internally, it initializes a Looper by calling the prepare(Boolean) method and placing it in the thread local variable sThreadLocal to determine whether sMainLooper has existed before. This is a basic singleton check. Obviously, we only allow Looper of the main thread to be instantiated once.
Similarly, a Looper on a non-main thread is only allowed to be instantiated once. When we instantiate a Looper on the non-main thread we call its static prepare() method. It also calls the prepare(Boolean) method to initialize a Looper and place it in the thread local variable sThreadLocal. Therefore, Looper instantiations of the main and non-main threads essentially call the same method, but they are implemented at different times and can only be instantiated once.
public static void prepare(a) {
prepare(true);
}
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));
}
Copy the code
After the above analysis, we can know that only one Looper can be instantiated for a thread, so when we create Handler instances in the same thread several times, they share one Looper. Or one Looper for multiple handlers.
2.3 MessageQueue Instantiation
MessageQueue is more complicated than Looper and Handler. Because of the internal use of JNI programming. Initialization, destruction, and enqueueing events all use native methods. You can see the source code definition in android_os_MessageQueue.
Every time we instantiate a Looper we call its constructor and instantiate a MessageQueue in it:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
Copy the code
As can be seen in the section on Instantiating Handlers, each time a Handler is instantiated, the MessageQueue is retrieved from the Looper corresponding to the current thread. So again, we can conclude that a Handler corresponds to a MessageQueue.
When we instantiate a MessageQueue we use its constructor. The native layer’s nativeInit() method is called to initialize the MessageQueue:
MessageQueue(boolean quitAllowed) {
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
}
Copy the code
In the native layer, the definition of nativeInit() method is as follows:
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
if(! nativeMessageQueue) { jniThrowRuntimeException(env,"Unable to allocate native queue");
return 0;
}
nativeMessageQueue->incStrong(env);
return reinterpret_cast<jlong>(nativeMessageQueue);
}
Copy the code
As you can see, the method instantiates a NativeMessageQueue and returns mPtr as a bridge between the Java MessageQueue and NativeMessesageQueue. This long member holds a Native instance, which is often used in JNI development. Therefore, MessageQueue also uses mPtr to represent message queues at the native layer. The partial definition of NativeMessageQueue at the native layer and its constructor are defined as follows.
class NativeMessageQueue : public MessageQueue.public LooperCallback {
/ /... Has nothing to do with the code
NativeMessageQueue::NativeMessageQueue() :
mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
mLooper = Looper::getForThread();
if (mLooper == NULL) {
mLooper = new Looper(false); Looper::setForThread(mLooper); }}Copy the code
As we can see from the above, NativeMessageQueue inherits from MessageQueue. And it instantiates a native layer Looper internally (its source is in Looper).
In the Native layer of Android, there exists a Looper similar to the Java layer. Its main function is to cooperate with the Looper of the Java layer to complete the most important thread communication in Android. When a message is stored in the message queue, the Looper in the Natvice layer is awakened. Looper in the Natvice layer blocks the entire thread when there is no message in the message queue or when the message has not reached the processing time. So, the thread that created Java Looper is active only when there are messages to be processed, and when there are no messages, the block is in the state of waiting for messages to be written. In this case, if we have Looper loops on the main thread, why not block the entire thread and cause ANR? This is because messages from our main thread are sent to the corresponding Looper of the main thread for processing, so in essence, many events in our main thread are sent as messages to the Handler of the main thread for processing. ANR occurs only when a message has been executed for too long to process other events.
Above we instantiate a Looper for the Native layer. In which the main logic is as follows:
void Looper::rebuildEpollLocked() {
// Close the previous epoll instance if it existed before
if (mEpollFd >= 0) {
mEpollFd.reset(); // Close the old epoll instance
}
// Apply for a new epoll instance and register the Wake Pipeline.
mEpollFd.reset(epoll_create(EPOLL_SIZE_HINT));
LOG_ALWAYS_FATAL_IF(mEpollFd < 0."Could not create epoll instance: %s", strerror(errno));
struct epoll_event eventItem;
// Set the unused data area to 0
memset(& eventItem, 0, sizeof(epoll_event));
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd.get();
// Add the wake event (mWakeEventFd) to the ePoll instance (mEpollFd)
intresult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem); LOG_ALWAYS_FATAL_IF(result ! =0."Could not add wake event fd to epoll instance: %s", strerror(errno));
// Input events such as keyboard and sensor Input are mainly added here. The system is basically responsible for these events, and rarely adds them voluntarily
for (size_t i = 0; i < mRequests.size(); i++) {
const Request& request = mRequests.valueAt(i);
struct epoll_event eventItem;
request.initEventItem(&eventItem);
// Add events from the request queue to the ePoll instance
intepollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem); }}Copy the code
This is where epoll comes in. Epoll is an extensible Linux I/O event notification mechanism for Multiplexing. It registers wake events with the corresponding FD in ePoll, and ePoll helps you listen for which wake events have messages arriving. The wake up event should be in non-blocking mode. In this way, the process blocks only when epoll is called, not when sending or receiving customer messages, and the entire process or thread is fully utilized. This is the event driven, so called response mode.
The epoll_ctl method is used in the above code to add the listener descriptor to the epoll handle. For epoll instructions, see epoll Mechanisms: epoll_CREATE, epoll_CTL, epoll_WAIT, Close. The main purpose of this piece of code is to create an ePoll instance and use it to listen for events to be triggered.
2.4 Message execution process
2.4.1 Process of message enqueuing
When we introduced the use of handlers, we also said that both Runnable and Message will eventually be meseaged and queued. So how is it executed after joining the queue?
First, let’s look at the process of joining the team. Here are the methods defined in Handler that are called to complete each time we queue a message.
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
As you can see, the enqueueMessage() method of the MessageQueue is actually used when queueing. So, let’s look at the definition of this method again:
boolean enqueueMessage(Message msg, long when) {
/ /... Irrelevant code, check
synchronized (this) {
/ /... Has nothing to do with the code
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;
prev.next = msg;
}
if(needWake) { nativeWake(mPtr); }}return true;
}
Copy the code
As you can see from the above method, the so-called enqueuing operation is essentially a logic to add a new message to the queue. Of course, this is done by ordering the messages according to when they are triggered. NeedWake is then used to determine whether to call the native layer method for the wake. Wake up only if there is a barrier before the current header message and the newly inserted message is the first asynchronous message to be triggered. In general, there is no need to wake up.
The nativeWake() method here will eventually call the awake() method of Looper on the native layer:
void Looper::wake() {
uint64_t inc = 1;
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
if(nWrite ! = sizeof(uint64_t)) {if(errno ! = EAGAIN) { LOG_ALWAYS_FATAL("Could not write wake signal to fd %d: %s", mWakeEventFd.get(), strerror(errno)); }}}Copy the code
This method writes a byte of content to mWakeEventFd. In other words, mWakeEventFd is readable. In other words, the thread of Looper in Native layer wakes up from the block state. We need to wake up because, each time we process a message, we wake up based on when the next message will execute. If the newly inserted message is the latest, then obviously we need to reset the wake up time. (Native Looper will enter the block state when we execute epoll_wait when we call the Java MessageQueue.)
2.4.2 Process of message execution
Above, we analyzed the process by which MessageQueue enqueues messages. When will these messages be executed? When we introduced the use of handlers, we also mentioned that when we instantiate Looper, we should call its loop() static method to process messages. So let’s look at the definition of this method.
public static void loop(a) {
final Looper me = myLooper();
/ /.. Has nothing to do with the code
final MessageQueue queue = me.mQueue;
/ /.. Has nothing to do with the code
for (;;) {
Message msg = queue.next(); // 可能会 bolck
if (msg == null) {
return;
}
/ /... Has nothing to do with the code
final long dispatchEnd;
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if(traceTag ! =0) { Trace.traceEnd(traceTag); }}/ /... Has nothing to do with the codemsg.recycleUnchecked(); }}Copy the code
As you can see above, when this method is called, it opens an infinite loop in which MessageQueue’s next() method is used to fetch and distribute the next message. Let’s skip the definition of the next() method here. Let’s analyze the part involved in this method first.
When the next message is received, it is processed by calling the dispatchMessage() method of the target, which is the Handler that sent the message. The method is defined 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
As you can see from the above, if the message is obtained by wrapping a Runnable, its handleCallback() method is directly called for processing. Runnable’s run() method is called directly within this method. Because it’s easier to see that, we subsidize the code.
Then, depending on whether the mCallback is empty, the decision is made whether to give it to the mCallback or the internal handleMessage() method. In this case, the mCallback is an interface that can be specified by the constructor when the Handler is created, which is relatively simple. The handleMessage() method, which we’re familiar with, is the method we overrode when we created the Handler to process messages. The message is then sent to our Handler for processing.
This is how the message is processed, and the logic of the code is fairly clear. Let’s see how MessageQueue gets the “next” message.
2.4.3 MessageQueue Message management
Above we have analyzed the execution process of the message sent by the Handler. Here we examine the logic for getting the “next” message:
Message next(a) {
// Return if the message loop has stopped. This can happen if the application tries to restart a Looper that has been stopped.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if(nextPollTimeoutMillis ! =0) {
Binder.flushPendingCommands();
}
// Calling a native method may cause this function to block
nativePollOnce(ptr, nextPollTimeoutMillis);
/ /... Has nothing to do with the code}}Copy the code
As you can see above, the Next () method of the Java layer’s MessageQueue is a loop. In addition to getting the message queue, it also listens for event firing of Natvie layer Looper. This is done by calling the native layer’s nativePollOnce() method. This method in turn calls the pollOnce() method of NativeMessageQueue internally. Also notice that in the following method, nativeMessageQueue is obtained from the mPtr of the Java layer. So we say that the mPtr we get when we initialize the MessageQueue acts as a bridge:
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
Copy the code
The pollOnce() method of native Looper is called in NativeMessageQueue, and the pollInner() method of native Looper is finally called:
int Looper::pollInner(int timeoutMillis) {
/ /... Adjust the timeout based on the event of the next message
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
mPolling = true; // Will be idle
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// The registered event was triggered or timed out
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
mPolling = false; // No longer idle
mLock.lock(); / / request lock
if (mEpollRebuildRequired) {
mEpollRebuildRequired = false;
rebuildEpollLocked(); // Rebuild epoll as needed
goto Done;
}
// Check
if (eventCount < 0) {
if (errno == EINTR) {
goto Done;
}
result = POLL_ERROR; / / error
goto Done;
}
if (eventCount == 0) {
result = POLL_TIMEOUT; / / timeout
goto Done;
}
// Process all messages
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd.get()) { // Wake fd to respond
if (epollEvents & EPOLLIN) {
awoken(); // The pipeline data is read and emptied}}else {
// Other input FD processing is actually to put the active FD into the RESPONSES queue and wait for processing
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
// Put the message into mResponses
pushResponse(events, mRequests.valueAt(requestIndex));
}
}
}
Done: ;
// Trigger all Message callbacks to process Native layer messages
mNextMessageUptime = LLONG_MAX;
while(mMessageEnvelopes.size() ! =0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
if (messageEnvelope.uptime <= now) {
{ / / get the handler
sp<MessageHandler> handler = messageEnvelope.handler;
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
mLock.unlock();
handler->handleMessage(message);
} / / release the handler
mLock.lock();
mSendingMessage = false;
result = POLL_CALLBACK;
} else {
// The queue header determines the next wake-up time
mNextMessageUptime = messageEnvelope.uptime;
break;
}
}
mLock.unlock(); / / releases the lock
// Trigger all response callbacks
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if (callbackResult == 0) {
removeFd(fd, response.request.seq); // Remove the file descriptor} response.request.callback.clear(); result = POLL_CALLBACK; }}return result;
}
Copy the code
From above we can see that the Native layer’s pollInner() method first puts the thread into the wait state by calling the epoll_wait method based on the timeoutMillis passed in by the Java layer. If timeoutMillis is not zero, then the thread enters the wait state. If an event is triggered, WAKE or another event that reuse Looper will be processed, so that the entire Looper in the Native layer is freed from the block state. Going back to the Java layer will continue the execution of the next statement in MessageQueue. As for when the Looper of Native layer wakes up from the block state, it needs to be determined according to the news that we join the team. We use the last few lines of MessageQueue’s enqueueMessage() method:
if (needWake) {
nativeWake(mPtr);
}
Copy the code
That is, wake up only if there is a barrier before the current header message and the newly inserted message is the first asynchronous message to be triggered.
Above is the logic related to Looper thread block of Native layer. When we get the next message from the message queue, we will determine the length of the thread block based on the time of the next message. When a message is added to the queue, we will adjust the length of the thread block based on the time of the new message and awaken the thread of the block if necessary. When the thread recovers from the block state, the Java Looper gets a message and processes it.
3, summarize
In the previous article, we analyzed the principle of Handler from the Java layer to the Native layer. So here’s a summary of this.
3.1 Relationship between Handler, MessageQueue and Looper
The first is the relationship between Handler, MessageQueue and Looper. Let’s use this diagram below:
That is, multiple Handler instances can be defined in a thread, but each Handler actually refers to the same Looper. Of course, we have to create Looper before we create Handler. Each Looper corresponds to only one MessageQueue. The MessageQueue is created when the Looper is created. MessageQueue uses Message objects to concatenate a unidirectional linked list structure, which in turn forms a Message queue. Each Message is a node in a linked list that encapsulates the Message we send.
3.2 Message sending process of the Handler
Then, let’s examine how messages are sent in the Handler. Again, we use a graph for the analysis:
The methods used by the Handler to send messages are divided into two types: POST and Send. Post is used to send Runnable data and Send is used to send Message data. But either type will eventually call the Handler’s sendMessageAtTime() method to queue MessageQueue. The difference is that methods of type POST need to be wrapped as messages by the Handler’s getPostMessage() before being sent.
3.3 Looper Execution Process
The message needs to be executed after it has been added to the queue, which is in Looper’s loop() method. But this part is a little more complicated because it involves something in the Native layer. Let’s use a graph again:
When we call Looper’s loop() method, the Looper loop starts processing messages. In the figure above is a loop that we have marked in green. When we call the Next () method of MessageQueue in the loop to get the next message, we will call the nativePollOnce() method, which may cause the thread to block and non-block. When the thread is non-blocking, it will go back from the Native layer to the Java layer. A message is retrieved from MessageQueuue and then processed by Looper. If fetching causes a thread to block, there are two ways to wake up the blocked thread. One is when a new message is added to the queue and is triggered before all the previous messages in the queue, and the timeout is reset. If the timeout period is reached, it can also be returned from the sleep state, returning to the Java layer to continue processing. Therefore, the function of Looper at the Native layer is to block Looper by blocking the process of obtaining messages from the message queue.
3.4 the last
Because this article analyzes not only the Java layer code, but also the framework layer code, so it is best to combine both sides of the source code together to see, so more helpful to their understanding. In the article above, we provided links to the online code for some classes, which are available on Google Source and require A VPN to browse. In addition, because the author’s level is limited, there are inevitably mistakes and deficiencies, welcome criticism and correction.