preface

Handler has plenty of applications in Android, both as a messaging mechanism and as a means of switching threads. Knowing how the upper-layer API sends and processes messages, as well as some of the problems and strategies associated with Handler, doesn’t really cause a problem without understanding Handler principles.

Handler plays a bigger role than described above, because the running process of an APP is the process of receiving and processing messages. Activities, for example, start, create, lifecycle callback, and destroy, all driven by Handler sending messages. From an APK installation, to a View update, are inseparable from the help of the Handler.

This article may be useful if you have any questions about:

  • How does the Handler ensure that it runs on the target thread
  • Handler May cause memory leaks
  • Why isn’t loop() blocked and the CPU busy waiting
  • How is MessageQueue stored
  • How to cache messages
  • What is a thread-free message
  • How do threads use the Handler mechanism

Note: This article source version is 8.0

How the Handler works

Handler role assignment:

There are four roles in Handler

Handler

Handler sends a message to Looper. After Looper processes the message, the Handler processes the message. The key upper API is handleMessage(), and subclasses implement their own processing logic.

Looper

Looper runs in the target thread, continuously reading messages from MessageQueue and assigning them to Handler. Looper acts as a link, aggregating messages from different channels into a target thread for processing. Therefore, Looper needs to ensure that the thread is unique.

MessageQueue

Stores the Message object Message, which determines how messages are extracted and stored when Looper fetches messages from MessageQueue or when Handler inserts data into it. MessageQueue also maintains the connection to the Native side and is the Java side controller for looper.loop () blocking.

Message

Message contains the specific Message data, and a Handler reference to send the Message is held in the member variable target. So when the message gets the time, you know which Handler is going to handle it. In addition, the static member variable sPool maintains a message cache pool for reuse.

Operation process

First, you need to build the message object. Message is obtained from the handler.obtainMessage () family of functions that provide function parameters corresponding to key member variables of the Message object. Regardless of which method is used, The concrete Message objects are finally obtained through message.obtain ().

Private static Message sPool; Private static int sPoolSize = 0; // Message next; public static Messageobtain() {// ensure synchronized (sPoolSync) {if(sPool ! = null) {// Cache pool is not empty Message m = sPool; // The cache pool points to the next Message node sPool = m.next; // The Message object from the cache is disconnected from the cache. m.flags = 0; // clearin-use flag // Cache pool size minus 1 sPoolSize--;returnm; }} // Cache eats no available object, returns new Message()return new Message();
    }
Copy the code

The Message member variable has a next of type Message. Message is a linked list, and the process of retrieving the Message object from the cache pool can be described in the following figure:

After the message is created, it is sent to the message queue by a Handler. There are many ways to send the message. There are two types of transmitter:

  1. Send the Message object to
  2. Send Runnable, which is wrapped in Message with getPostMessage() as a member variable callback
Private static Message getPostMessage(Runnable r) {// getPostMessage m = message.obtain (); // Remember Runnale, call m.callback = r when the message is executed;return m;
    }
Copy the code

Either way, the final Message queue MessageQueue only knows that it received the Message object Message. Instead, the message is added to the message queue, eventually via enqueueMessage().

private boolean enqueueMessage(MessageQueue queue, Message msg, MSG. Target = this; long uptimeMillis) {// message. target Remember the Handler to determine which Handler is handling the Message.if (mAsynchronous) {
            msg.setAsynchronous(true); } // The message is queuedreturn queue.enqueueMessage(msg, uptimeMillis);
    }
Copy the code

When enqueuing a message, it is sometimes necessary to provide delay information delayTime, which is stored in the uptimeMillis, for how long in the future it will be executed.

It then waits for Looper polling to read the message from the message queue for processing. See the stars. The loop ()

    public static void loop() {// get Looper 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();if(MSG == null) {// Message is empty, returnreturn; }... Try {/ / distribute messages to the Handler MSG. Target. DispatchMessage (MSG); end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); // Add the message to the cache pool msg.recycleunchecked (); }Copy the code

Looper retrieves the Message from MessageQueue, message.target is the concrete Hander, and handler.dispatchMessage () triggers the concrete allocation logic. After that, Message is reclaimed and put into the cache pool.

    public void dispatchMessage(Message msg) {
        if(msg.callback ! Runnable. Run () handleCallback(MSG); }else {
            if(mCallback ! = null) {// Specifies the Handler's mCallbackif (mCallback.handleMessage(msg)) {
                    return; }} // Handle handleMessage(MSG); }}Copy the code

Handler allocates messages in one of three ways:

  1. A Runnable message can be sent to a message queue via Handler, so handleCallback() handles this situation
  2. When assigning a message to Handler, Callback takes precedence over the message. If callback.handleMessage () returns true, handler.handleMessage () is no longer executed.
  3. Handler.handlemessage () handles the concrete logic

Recycling is passed via message.recycleunchecked ()

    void recycleUnchecked() {// here is the operation to reset the various properties of Message...... synchronized (sPoolSync) {if(sPoolSize < MAX_POOL_SIZE) {sPoolSize < MAX_POOL_SIZE) {sPoolSize < MAX_POOL_SIZE; sPool = this; sPoolSize++; }}}Copy the code

Based on the above analysis, the Handler is shown below

  1. The Handler retrieves the Message from the cache pool and sends it to MessageQueue
  2. Which reads the Message from the MessageQueue continuously, through Message. Target. DispatchMessage () the trigger Handler processing logic
  3. Reclaim Message to cache pool

Handler Handler Handler Handler Handler Handler Handler Handler Handler Handler Handler

The Connection between the Java end and the Native end is established

In fact, the Handler mechanism exists not only on the Java side, but also on the Native side. They established a connection through MessageQueue.

In general, Looper is initialized with prepare()

Private static void prepare(Boolean quitAllowed) {// Ensure that Looper is unique in the threadif(sThreadLocal.get() ! = null) { throw new RuntimeException("Only one Looper may be created per thread"); } // add Looper to ThreadLocal sthreadLocal. set(new Looper(quitAllowed)); }Copy the code

When instantiating Looper, you need to ensure that Looper is unique in the thread. The Handler knows its own specific Looper object, and the Looper runs in the specific thread in which the message is processed. This is why Looper can switch threads. The Looper Thread only needs a ThreadLocal to ensure that a Thread is provided with a member of type ThreadLocalMap, threadLocals, With ThreadLocal, objects can be put into threadLocals for K/V storage to ensure that variables are stored within the thread scope, where the Key is ThreadLocal< T >.

Private Looper(Boolean quitAllowed) {// Initialize MessageQueue mQueue = new MessageQueue(quitAllowed); // remember that the currentThread mThread = thread.currentthread (); }Copy the code
MessageQueue(boolean quitAllowed) { mQuitAllowed = quitAllowed; // Connect to Native mPtr = nativeInit(); }Copy the code

When MessageQueue is created, a connection is established with the native end through the native method nativeInit(). MPtr is a long variable and stores an address. Method implementation file is located in the frameworks/base/core/jni/android_os_MessageQueue CPP

static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
    if(! nativeMessageQueue) { jniThrowRuntimeException(env,"Unable to allocate native queue");
        return0; } nativeMessageQueue->incStrong(env); // Returns the Java layer's mPtr, NativeMessageQueue address valuereturnreinterpret_cast<jlong>(nativeMessageQueue); } NativeMessageQueue::NativeMessageQueue() : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) { mLooper = Looper::getForThread(); // Check whether Looper is createdif (mLooper == NULL) {
        mLooper = new Looper(false); // Ensure Looper only Looper::setForThread(mLooper); }}Copy the code

NativeMessageQueue was created on the Native side, as was Looper on the Native side. After NativeMessageQueue is created, its address value is returned to the Java layer messagequeue.mptr. In fact, the Native Looper does more when instantiated. Nativ end which file is located in the system/core/libutils/stars. The CPP

Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false), mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {// Add file descriptor to epoll, Thread awakens the events of fd mWakeEventFd = eventfd (0, EFD_NONBLOCK | EFD_CLOEXEC); LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0,"Could not make wake event fd: %s",
                        strerror(errno));

    AutoMutex _l(mLock);
    rebuildEpollLocked();
}

void Looper::rebuildEpollLocked() {
    .....

    // Allocate the new epoll instance and register the wake pipe.
    // 创建epolle实例,并注册wake管道
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno)); struct epoll_event eventItem; Memset (& eventItem, 0, sizeof(epoll_event)); // Eventitem. events = EPOLLIN; // Eventitem. events = EPOLLIN; // Set fd eventitem.data. fd = mWakeEventFd; Int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem); LOG_ALWAYS_FATAL_IF(result ! = 0,"Could not add wake event fd to epoll instance: %s", strerror(errno)); // Add FDS of various events to epoll instances, such as keyboard, sensor input, etcfor (size_t i = 0; i < mRequests.size(); i++) {
        const Request& request = mRequests.valueAt(i);
        struct epoll_event eventItem;
        request.initEventItem(&eventItem);

        int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem);
        if (epollResult < 0) {
            ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s", request.fd, strerror(errno)); }}}Copy the code

At first glance, the above code is confusing because it is missing the key point, the epoll mechanism.

How to understand the epoll mechanism?

Files, sockets, pipes, and other I/O objects can be considered streams. Since it is an I/O operation, the read side reads data and the write side writes data. But the two sides do not know the timing of the other operation. Epoll, on the other hand, can observe which stream has an I/O event and notify it. This process is just like when you are waiting for the express, but you don’t know when the express will come, then you can go to sleep, because you know the express will come to wake you up with a phone call, let you pick up the express, and then do what you want to do. Epoll effectively reduces CPU usage by putting it to sleep in thread-space and waking it up when an event arrives.

The epoll mechanism can be found here

Now that you know about epoll, look at the code above and it makes sense. When Looper is created on the Native side, fd — mWakeEventFd is created to wake up the thread, an epoll instance is created and the pipe is registered, the pipe data is cleared, and readable events are listened for. When data is written to a file described by mWakeEventFd, ePoll can listen for this event and notify the target thread to wake up.

MessageQueue. MPrt stores the address of the Native NativeMassageQueue on the Java end, and NativeMassageQueue can use this mechanism.

The process of sending data

Said before, send message Handler, ultimately through the MessageQueue. EnqueueMessage message into the message queue, for a specific code below

boolean enqueueMessage(Message msg, long when) { ...... synchronized (this) { ...... MSG. When = when; Message p = mMessages; // Wake up the thread flag bit Boolean needWake;if(p = = null | | the when = = 0 | | the when < p.w hen) {/ / here three of the following: MSG. Next = p; MSG. Next = p; MSG. mMessages = msg; NeedWake = mBlocked; }elseNeedWake = mBlocked && p.target == null && msg.isasynchronous (); Message prev; // The loop here is to find the insertion location of the messagefor(;;) { prev = p; p = p.next; // to the end of the list, or processing time earlier than pif (p == null || when < p.when) {
                        break;
                    }
                    if(needWake &&p.isasynchronous ()) {// If the inserted message is in the middle of the destination queue, needWake = does not need to be checked to change the thread wake statefalse; }} // Insert into message queue MSG. Next = p; prev.next = msg; }if(needWake) {// Wake up the thread nativeWake(mPtr); }}return true;
    }
Copy the code

Messages in message queues are also stored in linked lists, in chronological order of processing. When inserting data into a message queue, there are four cases:

  1. The target message queue is empty
  2. The inserted message processing time equals zero
  3. The inserted message processing time is less than the message processing time saved in the message queue header
  4. The inserted message processing time is greater than the message queue header

In the first three cases, the message is inserted into the message queue header, in which case there is no guarantee that the current message is in a processable state, and nativeWake() is called to wake up the thread if it is sleeping. In the latter case, you need to find where the message was inserted and change the thread state without affecting the thread state.

Insert message as shown

MPtr stores the address of NativeMessageQueue, so Native can know the specific NativeMessageQueue that is currently being used to wake up the thread, Actual invocation chain for MessageQueue. CPP. NativeWake () – > MessageQueue. CPP. Wake () – > stars. CPP. Wake ()

void Looper::wake() {
#if DEBUG_POLL_AND_WAKE
    ALOGD("%p ~ wake", this);
#endifuint64_t inc = 1; Ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &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, strerror(errno)); }}}Copy the code

The implementation is also simple, write a data to the mWakeEventFd file, according to the epoll mechanism to listen for the I/O event, wake up the thread.

The message read

Looper continuously reads messages from MessageQueue for processing and reads from messagequeue.next ().

    Message next() {
        final long ptr = mPtr;
        if(PTR == 0) {// Failed to obtain the address of NativeMessageQueue. The epoll mechanism cannot be used normallyreturnnull; PendingIdleHandlerCount = -1; pendingIdleHandlerCount = -1; // If this variable is equal to 0, it means that the current // thread does not enter the sleep wait state even if there are no new messages to be processed in the message queue. Int nextPollTimeoutMillis = 0; if the value is -1, then the current thread needs to wait indefinitely until it is woken up by another thread while there are no new messages in the message queue //.for(;;) {... NativePollOnce (PTR, nextPollTimeoutMillis) tries to hibernate nativePollOnce(PTR, nextPollTimeoutMillis). Synchronized (this) {// Final long now = systemclock. uptimeMillis(); synchronized (this) {final long now = systemclock. uptimeMillis(); Message prevMsg = null; Message MSG = mMessages;if(msg ! = null && msg.target == null) {// Find a valid Messagedo {
                        prevMsg = msg;
                        msg = msg.next;
                    } while(msg ! = null && ! msg.isAsynchronous()); }if(msg ! = null) {/** * checks the current time and the time at which the message is to be processed, if less than the current time, processing is imminent */if(now < msg.when) {// The time for the next message to be processed has not been reached, NextPollTimeoutMillis = (int) math.min (msg.when - now, integer.max_value); }else{// There is a message to process // do not enter hibernation mBlocked =false;
                        if(prevMsg ! = null) { prevMsg.next = msg.next; }else{// Point to the next message to be processed mMessages = msg.next; } msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        returnmsg; }}else{// No more messages, sleep time unlimited nextPollTimeoutMillis = -1; }...if(pendingIdleHandlerCount < 0 && (mMessages = = null | | now < mMessages. When)) {/ / get pendingIdleHandlerCount IdleHandler Numbers  = mIdleHandlers.size(); }if(pendingIdleHandlerCount <= 0) {// There is no IdleHandler to process and it can be directly thrown into hibernation mBlocked =true;
                    continue;
                }

                if(mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } // If there are no more messages to process, a thread idle message is sent to the IdleHandler object registered in the message queue for processing before sleepfor (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false; Keep = idler.queueidle (); } catch (Throwable t) { Log.wtf(TAG,"IdleHandler threw exception", t);
                }

                if(! Keep) {synchronized (this) {mIdleHandlers. Remove (idler); PendingIdleHandlerCount = 0; pendingIdleHandlerCount = 0; /** * set 0 to indicate that the next loop cannot go to sleep immediately, because the IdleHandler may send a new message while it is processing the event and need to check again. */ nextPollTimeoutMillis = 0; }}Copy the code

It is divided into two cases:

When the Message Message is fetched

You need to check whether the current time has reached the time for Message processing, and if so, return, changing mMessages to point to the next Message. If not, calculate the time to reach the processing, how long to sleep, and hibernate

There are no more messages

When there are no messages in the message queue, it checks to see if there are idleHandlers to process. In the Handler mechanism, allows you to add some events, the thread is idle for processing, characterized by IdleHandler, by MessageQueue. AddIdleHandler added. If an IdleHandler needs to be processed, the thread cannot sleep immediately after the IdleHandler is finished. During this period, new messages may be added to the message queue, and you need to check again. If there is no IdleHandler, you can go to sleep.

Thread dormancy invocation chain for NativeMessageQueue. NativePollOnce () – > NativeMessageQueue. PollOnce () – > stars. PollOnce () – > stars. PollInner ()

int Looper::pollInner(int timeoutMillis) { ...... Struct epoll_event eventItems[EPOLL_MAX_EVENTS]; Int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); . /** * Detects which file descriptor has an IO read/write event */for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeEventFd) {
            if(epollEvents & EPOLLIN) {// If the file descriptor is mWakeEventFd and the read/write event type is EPOLLIN, a new data was written to a pipe associated with the current thread // wake up awoken(); }}... }}Copy the code

The Java layer provides a thread sleep time, timeoutMillis, to sleep threads through epoll_wait(). When the thread is woken up, look at the file descriptor. If it is mWakeEventFd and an I/O event, it means that a new data has been written to a pipe associated with the current thread, which is processed by awoken(). While the current thread is awake, awoken() reads out the data in the pipe for cleaning purposes, but doesn’t care what the data is. The core purpose is to wake up the thread.

conclusion

The Handler mechanism is shown in the following figure:

  1. Looper is created with prepare() and is kept unique by ThreadLocal. If prepare() is not performed, Loop() will throw an exception
  2. Looper creates a MessageQueue during instantiation. The MessageQueue is connected to NativeMessageQueue, and the NativeMessageQueue storage address is stored in MessageQueue. MPtr. The Native end also establishes the Handler mechanism and uses the epoll mechanism. Java side can use epoll mechanism through nativemessaguE
  3. The Message is fetched from the Message cache, stored as a linked list, fetched from scratch, and the Message is inserted into the header when recycled. If it cannot be retrieved from the cache, create a new one
  4. Handler inserts a message to MessageQueue. If the message is inserted into the MessageQueue header, the thread needs to be woken up. If inserted into a message queue, there is no need to change the thread state
  5. Looper.loop() keeps getting messages from the message queue, and two things happen when the message queue gets messages. If a message is received but the processing time has not reached, the thread is put to sleep. If there are no more messages, consider putting the thread to sleep after processing the IdleHandler
  6. If the Message is Runnable, if the handle. Callback is set, and if the Message is normal, the Handler will process the Message. The corresponding calls are message.callback.run (), callback.handleMessage (), handler.handleMessage ()
  7. From the Handler mechanism, epoll can be simply interpreted as: when the Handler mechanism has no message to process, the thread is put to sleep, when the Handler mechanism has a message to process, the thread is called up. This is achieved by listening on the I/O event of mWakeEventFd at the Native end

Answering questions

Here are the answers to the questions at the beginning of the article

How does the Handler ensure that it runs on the target thread

Looper keeps threads unique through ThreadLocal when instantiated. Looper runs in the target thread, receiving messages sent by the Handler and processing them. Message creation is associated with a specific Handler, so you know which Handler is doing the corresponding work.

Handler May cause memory leaks

Message.target stores a reference to a Handler to know which Handler is handling it. Therefore, when a Handler is a non-static internal class, or holds some other representation of a key object (for example, an Activity often behaves as a Context), it refers to other external objects. While messages are not processed, external objects held by handlers remain memory leaky.

Why isn’t loop() blocked and the CPU busy waiting

The epoll mechanism listens for file I/O time. When a Message needs to be processed, data is written to wake up the thread. Put the thread to sleep when there are no messages to process.

How is MessageQueue stored

Stored in linked lists, MessageQueue. MMessages point to the header node.

How to cache messages

Cached as a linked list, removed from the head when retrieved, inserted into the head when reclaimed.

What is a thread-free message

Handler provides a mechanism for adding IdleHandler events. And let the IdleHandler do something specific, which is the event handled in thread-space, until there are no more messages to process and it goes to sleep.

How do child threads use the Handler mechanism

Make sure that looper.prepare () is executed before looper.loop () is used in subprograms, but there are few practical scenarios. Incidentally, the main thread initialization Looper operation is triggered in activityTrest.main (), which is easy to understand.

reference

“Android Source Code Scenario Analysis” — Chapter 13

Android Handler mechanism 10 Native implementation

The best Epoll Explanation I’ve ever read — from Zhihu