Handler source code analysis

Posted by Confusion101 on Mon, 07 Mar 2022 12:17:36 +0100

Handler parsing in Android

Understanding Handler

1. What is a Handler

Handler is a very important component of android messaging processing mechanism. It is often used by working threads to update UI threads and deliver messages, so as to realize the communication between threads.

2. Why use Handle

Multithreading is often involved in our daily development. How can multithreading update the UI? The android system requires that non UI threads cannot update the UI, otherwise an exception will be thrown. Only the original thread that created a view hierarchy can touch its views. For relevant logic, please refer to the ViewRootImpl source code, If the sub thread wants to update the UI, it can only notify the UI thread in some way, so this way is to use Handler, which can ensure that multiple threads update the UI concurrently and ensure thread safety.

3. Related concepts of Handler

Before learning about Handler, first take a look at the following concepts related to Handler. What is the main thread and what is the working thread?
Main thread: when the application starts, a UI thread will be created in the entry class ActivityThread to handle UI related events.
Sub thread: sub threads need to be created to perform time-consuming operations.
Message: message, the data unit of communication between threads.
Message Queue: Message Queue, queue for storing messages, first in first out.
Looper: the communication medium of circulator, Message Queue, MessageQueue and Handler. Loop out the message of the Message Queue and send the message to the corresponding Handler.
Handler: handler, the communication medium between the main thread and the working thread, the handler of the message, and adding the message to the Message Queue; Handle messages distributed by Looper.

So, how many questions can you answer,
1) Can each thread have multiple loopers?
2) Can a Looper bind multiple handlers?
3) Why doesn't the main thread initialize Looper and the child thread needs it?
4) Why does the Looper loop get the message without blocking the main thread and how to send the message to the corresponding thread? Or how to do thread switching?
If you can't, keep looking....

Handler mechanism process

The whole process is divided into several steps:
1) Preparation stage:

  • Create Looper object: the application startup will create Looper in the main thread by default and call Looper Loop(), so the main thread has a loop by default, but if the child thread needs to manually create a loop and call loop().
  • Initialize Message Queue: when creating Looper, you need to create the Message Queue of the current thread, which corresponds to Looper one by one.
  • Create Handler object: create a Handler object through the construction method. The parameterless construction method is bound to the Looper of the current thread. If the current thread has no Looper object, an exception will be thrown. Can't create Handler inside thread XX that has not called Looper Prepare(), so if you create a Handler in a child thread, you must first initialize Looper or bind the main thread Looper. The source code will be analyzed in detail later.

So far, the handler object is bound to the thread Looper and Message Queue.
2) Send message: the worker thread can send messages to the message queue through the Handler.
3) Message loop: the Looper loop takes out the messages in the queue. If there is no message available, it will be blocked. Otherwise, it will take out the message and send it to the Handler who created the message.
4) Processing message: the Handler receives the message sent by Looper and performs relevant business logic processing.

At this point, a complete message processing process is completed. The flow chart is as follows:

Use of Handler

Handler is also very simple to use. It is divided into three steps: creating objects, sending messages and receiving messages
1) Create object:
Looking at the Handler source code, we can find that there are seven construction methods of Handler, which can be divided into two categories. The first category: bind the Looper of the current thread, and directly obtain the Looper of the current thread in the Handler construction method. If it is the main thread, you don't need to initialize the Looper. If it is a sub thread, you must call Looper first Prepare() creates the Looper object of the current thread and binds it to the Handler object. Otherwise, an exception will be thrown:

Handler handler = new Handler(Looper.getMainLooper());

The second type is to bind the incoming Looper, which can be the Looper of the incoming main thread or the Looper created by the sub thread.

2) Send message:
Handler supports sending instant messages and delayed messages. The implementation principle will be introduced in the following source code:

handler.sendMessageDelayed(Message.obtainMessage(), 1000);

Or post

handler.post(Runnable r);

3) Receive message:
Receive messages and update UI through handleMessage() callback. If Runnable is used, it will be processed in the Runnable callback.

	@Override
        public void handleMessage(Message msg) {
           //Update UI
        }

Or post callback

post(new Runnable() {
     @Override
      public void run() {
               //Update UI      
	}
});

Source code analysis

The following mainly analyzes the implementation in the source code of handler, Looper and MessageQueue.

1,Handler

1) Construction method:
Bind Looper and MessageQueue

2) Send message:

2,Looper

1. Member variable of Looper:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static Looper sMainLooper;  // guarded by Looper.class
final MessageQueue mQueue;  //An MQ
final Thread mThread;    //A thread

2. Use
First, you need to initialize and call looper prepare();
Then start the loop, looper loop();
Note: the main thread has handled the above two steps by default. If you want to send it to the main thread Looper, you don't need to care about the above two points. If it is a sub thread, you need to implement the above two steps yourself.

3. prepare analysis

    public static void prepare() {
        prepare(true);
    }
 
    private static void prepare(boolean quitAllowed) {
        //Each thread can only have one looper
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //Set the looper of this thread
        sThreadLocal.set(new Looper(quitAllowed));
    }

Note: in the prepare(boolean) method, there is a sThreadLocal variable (ThreadLocal provides an independent copy of the variable for each thread using the variable, so each thread can change its own copy independently). This variable is a bit like a hash table,
Its key is the current thread, that is, it can store some data / references. These data / references correspond to the current thread one by one. The function here is,
It determines whether the current thread has a looper object. If so, it will report an error, "Only one Looper may be created per thread". A thread is only allowed to create one looper. If not, it will create a new one. Then it calls Looper's constructor.

4. Construction method of Looper
The constructor, Looper(quitAllowed), is called above, initializes the messageQueue and binds the current thread.

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

At this point, the initialization action is over. Next, look at looper loop():

5. loop() method parsing

    public static void loop() {
        final Looper me = myLooper();//Returns the current looper
        if (me == null) {
            //looper must first call prepare()
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;//Get messageQueue
         
        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();
        }
     }

Note:
1) In the loop() method, you need to get the Looper object corresponding to the current thread, so use the following myloop(),
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

2)MessageQueue queue = me.mQueue;
After getting the MQ corresponding to the Looper, it starts an infinite loop and continuously takes out messages from the message queue. When it gets a message, it calls message target. dispatchMessage () method to process the message. The dispatchMessage method in the Handler is to handle messages.

3)msg.recycleUnchecked(), when this msg is processed, it is useless. Recycle it to the global pool. Note that it is not destroyed.

3,MessageQueue

1. Member variable

private final boolean mQuitAllowed;//Indicates whether the MessageQueue is allowed to exit
private long mPtr; //mPtr is related to native code
Message mMessages; //Represents the header of the message queue

2. Construction method

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;  //true is allow push
        mPtr = nativeInit(); //This is a local method
    }

The construction method of MQ simply calls nativeInit() to initialize. This is a JNI method, that is, it may maintain the object of the message queue in the JNI layer. There are many native methods in message. You can see that message is a lower level class.

3. next() method

    Message next() {
        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();
            }
 
            nativePollOnce(ptr, nextPollTimeoutMillis);
 
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
 
                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }
 
                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }
 
                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }
 
            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler
 
                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }
 
                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
 
            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;
 
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

Note:
First initialize two variables to be used next, and then enter the infinite for loop. A loop mainly does the following things:

1) If nextpolltimeoutmillis= 0, call binder flushPendingCommands();

2) Call nativePollOnce(mPtr, nextPollTimeoutMillis);

3) Enter a large synchronization block and try to get a message that can be processed. The specific method is to record the current time now, the initialization variable prevMsg is null, and msg is mmesges;

If msg is a sync barrier message, go straight to the next asynchronous message (all synchronization messages between them will be ignored by this cycle, that is, in this case,

The next method will start from the location of the asynchronous message found, try to get a message that can be processed and return it), and update the values of prevMsg and msg;

4) When exiting this do... while loop, msg may be empty (at the end of the queue), or such a (asynchronous) message may be found successfully.

5) If it is at the end of the queue, i.e. msg==null, it means there are no more messages. Set nextPollTimeoutMillis = -1; Otherwise, when now < msg When (msg's time is not yet up), set a reasonable waiting time, that is, call

nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);

6) When it's time for msg processing, that is, we find such a message to return, set mblock to false, take msg out of the mMessages queue (similar to the deletion of single linked list), and execute it

msg.next=null,msg.markInUse(), return MSG.

7) If there is no return after this step, it indicates that there is no message that can be processed. Check whether the queue requires to exit. If it is to execute dispose(), it returns null. When the loop method of loop sees a null message, it will exit loop.

8) Next, since there is no message to handle, it's time to handle IdleHandler. If pendingIdleHandlerCount is less than 0 (note that it is initialized to - 1 when entering the for loop for the first time) and there are no more messages to process, set pendingIdleHandlerCount = midlehandlers size();

9) If pendingIdleHandlerCount is still < = 0, it means that there is no idle handler to execute,

Set blocked to true next time, and then enter the loop.

10) The next step is to initialize mPendingIdleHandlers according to mIdleHandlers. After exiting the synchronization block, we have only one last thing left, that is, run Idle handlers. A for loop is used to do this. If the IdleHandler does not need to be kept in the loop, it will be removed from the idlehandlers.

11) Finally, reset pendingIdleHandlerCount to 0 (that is, 4 will only be executed once in the first cycle), and set nextPollTimeoutMillis to 0, because when we

When executing 4, the new Message may have arrived, so we need to start the next cycle immediately (without waiting) to check.

4,enqueueMessage()

  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 {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                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;
    } 

When we introduce Handler later, we can see that many operations related to message (including runnable) are finally delegate d to the enqueue method of MessageQueue.

Like any method, the parameter msg is checked before enqueue. For example, if msg is in use or the target of msg is null, AndroidRuntimeException will be thrown. After the condition check, the real processing logic will enter. The next operation is similar to inserting an element into a single linked list: entering the synchronization block

1) If the queue is in the state of exiting, you can't join the queue or insert elements. In this case, RuntimeException will be thrown, and then return false,

Indicates failure;

2) Next, the status of the queue is ok, the when field of msg is set, and the temporary variable p points to the queue header; (necessary initialization and preparation)

3) If the queue is empty or when==0 or when < p.when, that is, the message to be inserted should be in the first position, that is, the Head of the queue, then it will be a new Head and connect it with the original queue;

4) Otherwise, the insertion will occur somewhere in the middle of the queue (possibly at the end of the queue). Insert msg in front of the first P, and P meets this condition (P = = null | when < p.when).

Finally, exit the synchronization block and return true, indicating that the operation (joining the queue) is successful.

5,quit()

  void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }
 
        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;
 
            if (safe) {
                removeAllFutureMessagesLocked();
            } else {
                removeAllMessagesLocked();
            }
 
            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }

Note: the quit method has two different exit strategies according to the value of the passed parameter safe. If it is safe, it will execute removeAllFutureMessagesLocked(). Its internal logic is that if the element at the head of the queue has not expired, it means that all other elements in the queue have not arrived, so it is equivalent to deleting all messages and calling
removeAllMessagesLocked(); Otherwise, traverse the queue and find the first message element P such as p.when > now (update the tail of the queue, p.next=null, shorten the queue). The elements to be deleted from P to the end of the queue are all deleted; If unsafe exits, all messages will be deleted and recycled directly, that is, removeallmessageslocked() will be called.

common problem

1. Why is the Looper of the main thread an dead loop, but not ANR?

Because when looper processes all messages, it will enter the blocking state. When a new Message comes in, it will break the blocking and continue to execute. This actually doesn't understand the concept of ANR. Anr, full name Application Not Responding. When I send a Message drawing UI to the main thread Handler and it is not executed after a certain time, an anr exception is thrown. Looper's dead loop is a loop that executes various transactions, including UI drawing transactions. Looper dead loop indicates that the thread is not dead. If looper stops the loop, the thread ends and exits. Looper's dead loop itself is one of the reasons to ensure that UI drawing tasks can be executed.

2. How does the Handler switch threads?

Use loopers of different threads to process messages. The execution thread of the code is not determined by the code itself, but the logic of executing the code is called by which thread, or the logic of which thread. Each Looper runs on the corresponding thread, so the dispatchMessage method called by different loopers runs on its thread.

3. What is the blocking wake-up mechanism of Handler?

The blocking wake-up mechanism of handler is a blocking wake-up mechanism based on Linux. This mechanism is also similar to the handler mechanism. Create a file descriptor locally, and then the waiting party listens to the file descriptor. The waking party only needs to modify the file, and the waiting party will receive the file to break the wake-up. It is similar to loop listening to MessageQueue and handler adding message.

4. What is IdleHandler?

The interface object that will be called back when the MessageQueue is empty or there is no Message to be executed at present. IdleHandler looks like a Handler, but it is actually an interface with a single method, also known as a functional interface:
public static interface IdleHandler {
boolean queueIdle();
}
A List in the MessageQueue stores the IdleHandler object. When there is no Message to be executed in the MessageQueue, it will traverse and callback all idlehandlers. Therefore, IdleHandler is mainly used to handle some lightweight work when the Message queue is idle.
For specific processing logic, please refer to the source code of MessageQueue.

Topics: Java Android