The principle of UI thread from the perspective of Android Handler/Looper

Overview

The Handler/Looper mechanism is a non-important and basic mechanism of the Android system. This mechanism is often used for reference even when developing application frameworks on rtos or linux operating systems. Through this mechanism, a thread can process events in a loop, and the event processing logic is in the Handler’s handleMessage. This article recommends analyzing the android8.1 source code to implement the mechanism.

Handler/Looper

Handler: As the name suggests, a class that handles message messages. Handler can also send messages.

Looper: As the name suggests, it is a loop body. This loop body is essentially to continuously take out messages from the Queue and distribute them to specific targets. The specific target to be distributed needs to be analyzed in detail. And there is a Thread object inside Looper. It is easy to understand that the loop body of Looper is executed in the thread.

MessageQueue: Message queue, the Message sent through the Handler is added to the queue, and is continuously taken out by the Looper running in the Thread thread, and is executed by the Handler.

Execution flow chart:

Application using Looper

Applications using Looper are divided into two situations, main thread and ordinary thread:

Normal thread
 * <p>This is a typical example of the implementation of a Looper thread,
  * using the separation of {@link #prepare} and {@link #loop} to create an
  * initial Handler to communicate with the Looper.
  *
  * 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();
  * }
  * }

This code is very typical in Android source code. There are three steps to use:

  1. Looper.prepare creates a thread-private Looper object.
  2. Create a Handler to process messages.
  3. Looper.loop starts working:The thread keeps looping to get messages and process messages. If there is no message processing, it will sleep.

The above code seems simple. The simpler the code, the more difficult it is to understand. We need to understand how the above three steps connect Thread, MessageQueue, Handler and Looper.

Looper.prepare source code
 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();
    }

You can see that prepare creates a Looper object for the current thread that calls prepare. When the Looper object is created, new MessageQueue and Thread objects are created internally. Pay special attention to the fact that mThread is the thread that calls prepare.

Handler object creation
 *
  * mHandler = new Handler() {
  * public void handleMessage(Message msg) {
  * // process incoming messages here
  * }
  * };

As you can imagine, the Handler here must be connected with the Looper created by prepare earlier. This depends on the Handler’s constructor:

public Handler();
public Handler(Callback callback);
public Handler(Looper looper);
public Handler(Looper looper, Callback callback);

We need to take a look at the Handler() constructor above:

 public Handler() {
        this(null, false);
    }
    public Handler(Callback callback, boolean async) {

        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

You can see a very important sentence in the default constructor, Looper.myLooper, which will obtain the Looper object of the current thread, which is the thread-private object created by Looper.prepare. At the same time, Handler’s MessageQueue comes from the current thread Looper’s MessageQueue.

Note: Thread and Handler have a 1:N relationship. For example, the UI thread can have multiple Handler objects.

Handler sends and processes messages

According to the above analysis, calling mHandler can send a message, and then process the message in the handleMessage function of the Handler. Let’s see how the above process is implemented.

After Looper obtains a Message from the MessageQueue, it will first call Handler.dispatchMessage to distribute the message. The latter will then distribute the Message to the corresponding responsible person according to the specific strategy. The source code is as follows:

Handler.java:

 /**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(Message msg) {
    }

    /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

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

Distribution strategy:

1. If the Meesage.callback object is not empty, it will be distributed to message.callback first. This situation corresponds to the runnable object sent by the mHandler.post interface: In this case, Message.callback is the runnable object of post

public final boolean post(Runnable r);
public final boolean postAtTime(Runnable r, long uptimeMillis);

public final boolean post(Runnable r)
{
   return sendMessageDelayed(getPostMessage(r), 0);
}

private static Message getPostMessage(Runnable r) {
   Message m = Message.obtain();
   m.callback = r;
   return m;
}

2. Handler.mCallback is not empty and is distributed to mCallback.handleMessage

3. If the first two do not exist, then handleMessage is called. For example, in our current ordinary thread scenario, if this method is overridden, our own Handler.handleMessage will be called. This situation mainly corresponds to the use of Handler send series interfaces:

public boolean sendMessageAtTime(Message msg, long uptimeMillis);
public final boolean sendMessageDelayed(Message msg, long delayMillis);

For example, use it as follows:
Message msg = mHandler.obtainMessage();
mHandler.sendMessageAtTime(msg,xxxx);

public final Message obtainMessage()
{
    return Message.obtain(this);
}

public static Message obtain(Handler h) {
    Message m = obtain();
    m.target = h;

    return m;
}

You can see that Message.target = mHandler in this case, and Looper distributes code in the loop:

 */
    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;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            ...
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ...
            msg.recycleUnchecked();
        }
    }

The loop loop obtains the message from Looper’s MessageQueue, and then calls Message.target.dispatchMessage. As analyzed above, Message.target = mHandler, so the Handler.dispatchMessage method is called.

Summary:

1. Handler.post(runnable), in the Handler.dispatchMessage method, since it is distributed to the runnable object first, this runnable object is executed instead of Handler.handleMessage.

2. If you use the sendMessage interface, then Handler.dispatchMessage is distributed to the handleMessage interface of Handler override, which is the Handler’s handleMessage function in the code used by ordinary threads.

UI thread

We know that ActivityThread is the entry point of the main thread. Let’s take a look at the source code of Looper in the android UI thread:

 public static void main(String[] args) {

        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

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

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
      

Similar to using Looper with ordinary threads, it is also divided into 3 steps:

1. Prepare Looper: Looper.prepareMainLooper

2. Create a private Handler for the main thread: sMainThreadHandler = thread.getHandler

3. Start working: Looper.loop

Differences from ordinary threads:

1. Ordinary threads use Looper.prepare, while the main thread needs to use Looper.prepareMainLooper

2. Handler is different: a normal thread can generate a Handler bound to Looper, while the main thread obtains it from the current thread

Let’s analyze the above two differences separately.

prepareMainLooper
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            sMainLooper = myLooper();
        }
    }

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

As you can see, prepareMainLooper also uses prepare, but the parameter false means that the ui thread cannot exit, and the created Looper must be assigned to sMainLooper, so that other threads can obtain the object by calling getMainLooper.

MainThreadHandler

When the ActivityThread object is created, a Handler object will be generated internally:

final H mH = new H();

What thread.getHandler obtains is the mH, which means that one of the Handlers used by the main thread in ActivityThread to process messages is the Handler mH.

loop

Before analyzing the loop function, let us mention that a typical main function of a graphics window is as follows:

main() {
    initialize()//Initialization
    CreateWindow();//Create window
    ShowWindow();
    while(GetMessage()) {
        //Continuously distribute processing
        DispatchMessage();
    }
}

Then according to the analysis of the previous loop function, in fact, the loop function of the UI thread also continuously fetches and processes messages in the loop, which is consistent with the above model.

ViewRootImpl

We know that ViewRootImpl also belongs to the scope of the UI thread, because ViewRootImpl is created in the UI thread. According to the default constructor created by Handler, we know thatthe default constructor of Handler() is suitable for the Looper binding of the current thread. Yes, then the Handler constructed in the form of Handler() in ViewRootImpl also processes related messages of the UI thread.

final ViewRootHandler mHandler = new ViewRootHandler();
 final class ViewRootHandler extends Handler {
    public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_INVALIDATE:
                ((View) msg.obj).invalidate();
                break;
            case MSG_INVALIDATE_RECT:
                final View.AttachInfo.InvalidateInfo info = (View.AttachInfo.InvalidateInfo) msg.obj;
                info.target.invalidate(info.left, info.top, info.right, info.bottom);
                info.recycle();
                break;
            case MSG_PROCESS_INPUT_EVENTS:
                mProcessInputEventsScheduled = false;
                doProcessInputEvents();
                break;
            case MSG_DISPATCH_APP_VISIBILITY:
                handleAppVisibility(msg.arg1 != 0);
                break;
            case MSG_DISPATCH_GET_NEW_SURFACE:
                handleGetNewSurface();
                break;
            ....

Choreographer

Choreographer is also an important class that works on the UI main thread. It is related to Vsync. Let’s look at the source code to see why this class works on the UI main thread.

To see if it is the main thread, we need to check which Thread the Handler/Looper in Choreographer is based on. Look at the constructor of Choreographer as follows:

    private Choreographer(Looper looper, int vsyncSource) {
        mLooper = looper;
        mHandler = new FrameHandler(looper);
        mDisplayEventReceiver = USE_VSYNC
                ? new FrameDisplayEventReceiver(looper, vsyncSource)
                : null;
        mLastFrameTimeNanos = Long.MIN_VALUE;

        mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());

        mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
        for (int i = 0; i <= CALLBACK_LAST; i + + ) {
            mCallbackQueues[i] = new CallbackQueue();
        }
    }

So the key point is where Looper is built in the construction function. Choreographer is a singleton class, so look at its initialization code:

 // Thread local storage for the choreographer.
    private static final ThreadLocal<Choreographer> sThreadInstance =
            new ThreadLocal<Choreographer>() {
        @Override
        protected Choreographer initialValue() {
            Looper looper = Looper.myLooper();
            if (looper == null) {
                throw new IllegalStateException("The current thread must have a looper!");
            }
            return new Choreographer(looper, VSYNC_SOURCE_APP);
        }
    };

That is to say, what Looper.myLooper finally obtains is the private Looper object of the current thread, and Choreographer’s getInstance is called in the public ViewRootImpl(Context context, Display display) constructor. According to the ViewRootImpl section, we know that ViewRootImpl is built in the main thread. So Choreographer is on the main thread

Vsync signal processing

We all know that the actual logic of vsync signal processing is in the UI main thread. Let’s take a look at the specific implementation. First, we need to look at the place where vsync signals are received and processed. This is in Choreographer:

 private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {

            ...
            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }

It can be seen that after receiving the vsync signal, mHandler is used to send a message to the thread where the Handler is located. According to the Choreographer section, we know that the mHandler and its Looper belong to the UI thread, so the message sent in the above code is processed in the main thread, and FrameDisplayEventReceiver is a runnable object, so the final processing function for the main thread to receive this message is: doFrame function:

 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

The vsync signal is processed in the main thread. The final processing function is doFrame->doCallbacks, and doCallbacks is to process various events that the application has requested and recorded in the queue. For example, when the application requests rendering, the invalidate call stack is called:

invalidate() : ViewRootImpl.java
    --->scheduleTraversals() : ViewRootImpl.java
        --->mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

The final implementation of postCallback:

private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//Encapsulate the callback to be executed into a CallbackRecord object and save it in the mCallbackQueues array
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
//Function execution time is up
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {//Achieve delayed execution of functions through asynchronous messages
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}

1. Save the requested event in mCallbackQueue, wait until the vsync signal arrives, and take it out of doFrame for execution.

2. Send the MSG_DO_SCHEDULE_CALLBACK message, and finally call the native layer’s mDisplayEventReceiver.scheduleVsync(); to request to receive the next vsync signal