Android Looper and Handler Tutorial

Photo by Willian Justen de Vasconcellos on Unsplash
Photo by Willian Justen de Vasconcellos on Unsplash
Looper and Handler are one of the Android core components, and many high-level components are built on top of them. Understanding them helps us understand how some core components work. This article will introduce Looper and Handler and related components.

Looper and Handler are one of the Android core components, and many high-level components are built on top of them. Understanding them helps us understand how some core components work. This article will introduce Looper and Handler and related components.

Overview

Android’s Looper and Handler are very similar to ordinary threads, the main difference is that they maintain a MessageQueue, and process Message one by one. Therefore, when we need to perform multiple asynchronous tasks and want to avoid race conditions, Looper and Handler may be a good choice. Android’s main thread is implemented based on them. In addition, AsyncTask is also built on top of Looper and Handler.

The usage of Looper and Handler is very simple. Let’s look at a simple example first.

Declare MyHandler, inherit Handler, and override handleMessage(Message). When a message is to be processed, handleMessage(Message) is called, and we must implement how to handle the message inside.

class MyHandler(looper: Looper) : Handler(looper) {
    override fun handleMessage(msg: Message) {
        // Implementation to handle the message.
    }
}

Next, instantiate a HandlerThread and call HandlerThread.start() to start it. HandlerThread contains a Looper, and Looper contains a MessageQueue. At this time, HandlerThread.run() calls Looper.loop(), and Looper.loop() starts to receive Message one by one from the MessageQueue and process them. However, so far, the MessageQueue is still empty, so the HandlerThread will be blocked until the MessageQueue returns a Message.

Instantiate a MyHandler we just implemented and bind it to the Looper in HandlerThread. When we want to create a Message, we will not create the Message directly, but get a Message by calling Handler.obtainMessage(). It will automatically set Message.target to itself. So, when Looper.loop() is processing the Message, it will know which Handler to call to process the Message. Then call Handler.sendMessage() to send the Message to the MessageQueue in the bound Looper.

Finally, we have to call HandlerThread.quitSafely() to stop the HandlerThread. It will empty MessageQueue, and then MessageQueue returns null to Looper.loop(). When Looper.loop() receives null, it will stop receiving Message, so HandlerThread will stop.

// Initialize
val handlerThread = HandlerThread("HandlerThread", Process.THREAD_PRIORITY_BACKGROUND)
handlerThread.start()
val myHandler = MyHandler(handlerThread.looper)

// Send a message
val message = myHandler.obtainMessage().apply { arg1 = 1 }
myHandler.sendMessage(message)

// Quit the HandlerThread when you don't need it
handlerThread.quitSafely()

If you just want to know how to use Looper and Handler, you have all here. The internal working mechanism is described below.

Architecture

The diagram below shows the relationship between all related components.

Looper and Handler Architecture
Looper and Handler Architecture

The figure below shows the flow between these components. When HandlerThread starts, HandlerThread.run() will call Looper.parepare() to initialize Looper, and then call Looper.loop().

Inside Looper.loop() is a for loop, which always calls loopOnce(). If loopOnce() returns false, then loop() ends the for loop. loopOnce() calls MessageQueue.next() to receive the next pending Message. The type of Message.target is Handler. After receiving a Message, loopOnce() calls Message.target.dispatchMessage(), which calls Message.target.handleMessage() to process the Message.

When MessageQueue is empty, next() will be blocked, so that HandlerThread will be blocked. However, when HandlerThread.quitSafely() is called, it empties the MessageQueue and sets MessageQueue.mQuitting true. Moreover, if next() is blocked at this time, it will be woken up. When next() checks that mQuitting is true, it returns null. When loopOnce() receives null from next(), it returns false to loop(). Finally, loop() ends the for loop.

Flow of Looper and Handler
Flow of Looper and Handler

Message

Below is the code snippet of Message. We removed a lot of code, if you want to see the complete code, please refer to Message.java.

public final class Message implements Parcelable {
    public int what;
    public int arg1;
    public int arg2;
    public Object obj;
    public Messenger replyTo;

    static final int FLAG_IN_USE = 1 << 0;
    int flags;
    public long when;
    Bundle data;
    Handler target;
    Runnable callback;
    Message next;

    private static Message sPool;
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 50;

    public static Message obtain() {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0;
            sPoolSize--;
            return m;
        }
        return new Message();
    }

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

    public void recycle() {
        if (isInUse()) {
            return;
        }
        recycleUnchecked();
    }

    void recycleUnchecked() {
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        when = 0;
        target = null;
        callback = null;
        data = null;
        if (sPoolSize < MAX_POOL_SIZE) {
            next = sPool;
            sPool = this;
            sPoolSize++;
        }
    }

    public Message() {
    }
}

MessageQueue

Below is the code snippet of MessageQueue. We removed a lot of code, if you want to see the complete code, please refer to MessageQueue.java.

next() will be blocked at calling nativePollOnce() until it is woken up by nativeWake(). Then, it will try to receive and return the next pending Message. If the queue is empty, call nativePollOnce() to continue waiting, or return null when mQuitting is found to be true. nativePollOnce() and nativeWake() are JNI methods, interested readers can refer to Looper.cpp.

In quit(), removeAllFutureMessagesLocked() is called when safe is true, and removeAllMessagesLocked() is called when it is false. The difference is that removeAllMessagesLocked() removes all pending Message. And removeAllFutureMessagesLocked() only removes the Message whose Message.when is greater than the current time, so those Messages whose Message.when is less than the current time will still be processed.

public final class MessageQueue {
    private final boolean mQuitAllowed;
    Message mMessages;
    private boolean mQuitting;

    private native void nativePollOnce(long ptr, int timeoutMillis);
    private native static void nativeWake(long ptr);

    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
    }

    Message next() {
        for (; ; ) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null);
            }
            if (msg != null) {
                if (now >= msg.when) {
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    return msg;
                }
            }

            if (mQuitting) {
                return null;
            }
        }
    }

    void quit(boolean safe) {
        if (!mQuitAllowed) throw new IllegalStateException("Main thread not allowed to quit.");
        if (mQuitting) return;

        mQuitting = true;
        if (safe) {
            removeAllFutureMessagesLocked();
        } else {
            removeAllMessagesLocked();
        }
    }

    private void removeAllMessagesLocked() {
        Message p = mMessages;
        while (p != null) {
            Message n = p.next;
            p.recycleUnchecked();
            p = n;
        }
        mMessages = null;
    }

    private void removeAllFutureMessagesLocked() {
        final long now = SystemClock.uptimeMillis();
        Message p = mMessages;
        if (p != null) {
            if (p.when > now) {
                removeAllMessagesLocked();
            } else {
                Message n;
                for (; ; ) {
                    n = p.next;
                    if (n == null) {
                        return;
                    }
                    if (n.when > now) {
                        break;
                    }
                    p = n;
                }
                p.next = null;
                do {
                    p = n;
                    n = p.next;
                    p.recycleUnchecked();
                } while (n != null);
            }
        }
    }

    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.");

        if (mQuitting) {
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
        } else {
            Message prev;
            for (; ; ) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
            }
            msg.next = p;
            prev.next = msg;
        }

        return true;
    }
}

Handler

Below is the code snippet of Handler. We removed a lot of code, if you want to see the complete code, please refer to Handler.java.

All post and send methods call enqueueMessage() at the end. Handler sets itself to Message.target, and then call MessageQueue.enqueueMessage() to put the Message into the queue for processing.

When the Message is to be processed, Looper.loopOnce() calls Message.target.dispatchMessage() which is Handler.dispatchMessage(), which calls Handler.handleMessage(). This method is empty, because it is for developers to implement the code for processing Message.

public class Handler {
    private static Handler MAIN_THREAD_HANDLER = null;

    final Looper mLooper;
    final MessageQueue mQueue;
    final Callback mCallback;
    final boolean mAsynchronous;

    public interface Callback {
        boolean handleMessage(Message msg);
    }

    public void handleMessage(Message msg) {
    }

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

    public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

    public static Handler getMain() {
        if (MAIN_THREAD_HANDLER == null) {
            MAIN_THREAD_HANDLER = new Handler(Looper.getMainLooper());
        }
        return MAIN_THREAD_HANDLER;
    }

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

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

    public final boolean postAtTime(Runnable r, long uptimeMillis) {
        return sendMessageAtTime(getPostMessage(r), uptimeMillis);
    }

    public final boolean postDelayed(Runnable r, long delayMillis) {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }

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

    public final boolean sendMessage(Message msg) {
        return sendMessageDelayed(msg, 0);
    }

    public final boolean sendMessageDelayed(Message msg, long delayMillis) {
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();
        return queue.enqueueMessage(msg, uptimeMillis);
    }
}

Looper

The following is the code snippet of Looper. We removed a lot of code, if you want to see the complete code, please refer to Looper.java.

loop() keeps calling loopOnce() until it returns false. Instead, loopOnce() receives a Message from MessageQueue.next() and calls Message.target.dispatchMessage() to process the Message.

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

    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        sThreadLocal.set(new Looper(quitAllowed));
    }

    public static void prepareMainLooper() {
        prepare(false);
        sMainLooper = myLooper();
    }

    public static Looper getMainLooper() {
        return sMainLooper;
    }

    private static boolean loopOnce(Looper me, long ident, int threshold) {
        Message msg = me.mQueue.next();
        if (msg == null) return false;

        msg.target.dispatchMessage(msg);

        msg.recycleUnchecked();

        return true;
    }

    public static void loop() {
        Looper me = myLooper();
        me.mInLoop = true;

        for (; ; ) {
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

    public static Looper myLooper() {
        return sThreadLocal.get();
    }

    public static MessageQueue myQueue() {
        return myLooper().mQueue;
    }

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

    public boolean isCurrentThread() {
        return Thread.currentThread() == mThread;
    }

    public void quit() {
        mQueue.quit(false);
    }

    public void quitSafely() {
        mQueue.quit(true);
    }

    public MessageQueue getQueue() {
        return mQueue;
    }
}

HandlerThread

Below is the code snippet of HandlerThread. We removed a lot of code, if you want to see the complete code, please refer to HandlerThread.java.

HandlerThread.run() calls Looper.prepare() first and then Looper.loop(), the logic is quite simple. Although you can also declare a class yourself and inherit Thread, it is recommended to use HandlerThread.

public class HandlerThread extends Thread {
    int mPriority;
    int mTid = -1;
    Looper mLooper;
    private Handler mHandler;

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }

    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }

    protected void onLooperPrepared() {
    }

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

    public Looper getLooper() {
        return mLooper;
    }

    public Handler getThreadHandler() {
        if (mHandler == null) {
            mHandler = new Handler(getLooper());
        }
        return mHandler;
    }

    public boolean quit() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quit();
            return true;
        }
        return false;
    }

    public boolean quitSafely() {
        Looper looper = getLooper();
        if (looper != null) {
            looper.quitSafely();
            return true;
        }
        return false;
    }
}

Conclusion

After reading this article, did you find that the entire mechanism of Looper and Handler is not too complicated? It’s just that references between components can easily confuse you. Looper and Handler are very useful tools when we need to perform some tasks asynchronously and do not want race conditions to occur.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like