Analyzing Handler mechanism from source code

Posted by tullmejs on Wed, 16 Feb 2022 17:56:29 +0100

Handler is a technology that must be used in Android development. It is also very simple to use. Rewrite handler's handleMessage method in the main thread and send messages through handler's sendMessage() method in the sub thread.

The Handler mechanism can be well understood through this figure. There are several roles: ActivityThread, Handler, Message, messagequeue and Looper. First, briefly introduce these roles:

ActivityThread: the startup entry of the program. This class is what we usually call the main thread (UI thread). The operation of updating UI must be carried out in the main thread

Handler: it literally means that the operator should send messages to messagequeue through handler's sendMessage() method in the sub thread, and process the messages by overriding handler's handleMessage() method. Hold the objects of messagequeue and Looper in the handler
Messagequeue: Message queue is a queue for storing messages. It inserts messages sent by the handler, and Looper circulates messages from this queue
Message: that is, message. Several fields are defined in message to represent different information.

ActivityThread.main()

public static void main(String[] args) {
		//......
		//Only relevant codes are posted
        Looper.prepareMainLooper();
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        Looper.loop();
        //.....
    }

The main() method is the entry of the program. By looking at the main() method of ActivityThread, you can find that the preparemainloop () method and loop() method of the Looper class are called here. Next, look at the Looper class

Looper

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

These fields are included in Looper

ThreadLocal: each thread has a ThreadLocal, which is used to save the Looper object of the current thread
Messagequeue: message queue
Thread: current thread object
Next, look at the preparemainloop () and loop() methods

public static void prepareMainLooper() {
        prepare(false);//The prepare method is called here
        synchronized (Looper.class) {
            if (sMainLooper != null) {//Here, judge whether sMainLooper is empty
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();//If it is empty, call the myloop () method to fetch the Looper object from ThreadLocal
        }
    }


private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
            //It can be found that the prepare method can only be called once, that is, there can only be one Looper in a thread, otherwise an exception will be thrown
        }
        sThreadLocal.set(new Looper(quitAllowed));//If there is no Looper object in ThreadLocal, a new one will be added
    }

public static @Nullable Looper myLooper() {
        return sThreadLocal.get();//Take out the Looper object
    }
public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        for (;;) {
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

	//Here are the methods in loopOnce
	//Loop out messages from mqune (mqune is the messagequeue field mentioned above)
	Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }
	//Distribute the Message through the target of the Message (that is, the Handler that sends the Message)
	msg.target.dispatchMessage(msg);
	//Here, the representations and parameters in msg are cleared
	msg.recycleUnchecked();

Through the above analysis, we find that the loop method will take out the internal message sequencer, iterate the messages, and distribute them according to the target attribute of msg. But now there is a problem: where do the messages in messagequeue come from? This is our Handler.

Handler

First, let's look at the field of Handler

	final Looper mLooper;//Pass messagequeue through Looper
    final MessageQueue mQueue;//The Handler does not instantiate, but points to the Looper's messagequeue
    final Callback mCallback;
    final boolean mAsynchronous;
    IMessenger mMessenger;

Next, let's look at the constructor of Handler

public Handler(@Nullable Callback callback, boolean async) {
		//.............
        mLooper = Looper.myLooper();//Take the Looper object from the ThreadLocal of the current thread
        if (mLooper == null) {
        /*
        *Note: if it is in the main thread, the system has called looper by default Prepare () method, but it is created in the child thread
        *We need to call the prepare() method manually, otherwise the following exception will be thrown. You can also pass in the current exception when creating the Handler object
        *Looper object of thread
        */
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;//Messagequeue pointing to Looper
        mCallback = callback;
        mAsynchronous = async;
    }

Next, let's look at how the Handler sends messages. In fact, whether we call sendMessage() or other methods, we will eventually go to the sendMessageAtTime() method

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

The sendMessageAtTime() method receives two parameters, a Message object and a time parameter, indicating the time of sending the Message (this value is the current system time + delay time, such as when calling the sendMessageDelayed method), and then passes both parameters into the enqueueMessage method. After entering this method, have a look

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        //Specify the Target attribute of msg as this, that is, the current Handler, which corresponds to the loop method in loop
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
        //Queue msg through messagequeue, and you will know where the messages in messagequeue come from
    }

Summary:

  • There can only be one Looper object in a thread, but there can be multiple handlers
  • A Looper can bind multiple handlers, whereas a Handler can bind only one Looper (that is, the Looper of the current thread)

After creating the Handler, send the Message through the Handler, store the Message in the messagequeue inside the Handler, and then loop out the Message from the messagequeue through the Looper, and then process the Message through the Handler's handleMessage() method to complete the UI update
Personal understanding!!!!

Topics: Java Android UI