Looper
and Handler
are one of the key low-level components of Android. For example, the UI thread is built with them. However, only a few developers use them directly nowadays. In this article, we’ll try to understand how they work.
Brief overview
The Looper class is essentially an event loop for a thread. There can be at most one Looper
per thread.
For each Looper
, there is exactly one MessageQueue, which holds the list of messages to be dispatched. We rarely need to use this class directly.
For most of the time, we use the Handler class to send messages to a Looper
, and handles the messages when dispatched. There can be as many Handler
s as we want for any Looper
.
Sitting on top of them, the HandlerThread provides a thread with a Looper
. Let’s start from this class.
HandlerThread
The following code shows how to use a HandlerThread
in a very basic way:
val handlerThread = HandlerThread("my handler thread")
// need to call start() to start the thread
handlerThread.start()
val handler = object : Handler(handlerThread.looper) {
override fun handleMessage(msg: Message) {
println("handle message: $msg")
}
}
// the messages will be handled by Handler.handleMessage()
handler.sendMessage(Message.obtain(handler, 777))
// the runnable will just run
handler.post { println("a runnable") }
As we can see here, there are two main types of messages: a Message
that will be handled by the Handler
, or a Runnable
that will just run. We will discuss later how the messages are dispatched in more detail.
A HandlerThread
runs as long as the Looper
is running. There are two methods to stop the event loop and end the thread:
quit()
: discard all messages, and immediately quit the event loop.quitSafely()
: dispatch all messages that are already due, and then quit.
Handler
Send messages
As seen in the previous section, there are two main types of messages that Handler
can handle. When the user posts a Runnable
, it will first be wrapped into a Message
:
public class Handler {
public final boolean post(@NonNull Runnable r) {
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
...
}
Then it goes through the same code path as a regular Message
, and eventually asks the MessageQueue
to enqueue it:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
Handle messages
When Looper
dispatches a message, Handler.dispatchMessage()
will be called (modified for the sake of simplicity):
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
// if the Message is a Runnable, run it
msg.callback.run();
} else {
if (mCallback != null) {
// if the Handler has a Callback (set in constructor),
// try using it to handle the message
if (mCallback.handleMessage(msg)) {
return;
}
}
// as a last resort, calls handleMessage()
handleMessage(msg);
}
}
Note that the dispatchMessage()
method is NOT final
. So when subclassing Handler
, don’t confuse it with handleMessage()
.
Looper
To create our own thread with a Looper
, we can take the HandlerThread
as a reference:
public class HandlerThread extends Thread {
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
...
}
The most important things here are: 1) call Looper.prepare()
to initialize the looper; 2) call Looper.loop()
to run the event loop.
The prepare()
method basically ensures there is at most one Looper
per thread, stores the created Looper
in a ThreadLocal
, and creates a MesssageQueue
:
public final class Looper {
public static void prepare() {
prepare(true);
}
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();
}
...
}
Here, the quitAllowed
flag is true
for all the loopers in worker threads, meaning we can quit the event loop by calling quit()
or quitSafely()
.
The flag is false
only for the main thread for obvious reasons. Note that we should never call Looper.prepareMainLooper()
to mark current thread’s looper as the main looper. Fortunately, this method is finally makes as deprecated in R.
After the looper is initialized, we need to call the loop()
method to start the event loop, which has quite some logging code. However, the most interesting part is quite easy to understand, as shown below (modified for the sake of simplicity):
public static void loop() {
final MessageQueue queue = myLooper().mQueue;
for (;;) {
// fetches the next message to process
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// dispatches the message
msg.target.dispatchMessage(msg);
// recycles the message
msg.recycleUnchecked();
}
}
MessageQueue
MessageQueue
has two main methods:
enqueueMessage()
: called byHandler
to add messages to the queue, and wake up the queue if currently blocked.next()
: called byLooper
to get the next message to dispatch. If there is no message to dispatch, it will block waiting for messages.
IdleHandler
One thing interesting here is, when the MessageQueue
is going to block waiting for more messages to dispatch, it will first run IdleHandlers.
This is quite useful, when e.g. you need to do something on the main thread, but want to do it only when it’s idle. For example, ActivityThread
uses this technique to schedule garbage collection
Use case: IPC
IPC (inter-process communication) is one of the most common use cases for Looper
and Handler
, with the help of Messenger. Under the hood, the Messenger
class is simply a wrapper around a Binder
, which is used to perform the communication.
First, we create a remote service like below:
class RemoteService : Service() {
// the Handler to handle the messages
private val handler = object : Handler() {
override fun handleMessage(msg: Message) {
println("Message: $msg")
}
}
// the created Messenger will dispatch incoming messages to the handlers
private val messenger = Messenger(handler)
// when someone binds to the service, we return the IBinder backing the
// messenger, so that the peer can use to send messages
override fun onBind(intent: Intent?): IBinder? = messenger.binder
}
And we declare the service in AndroidManifest.xml
:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<application ... >
<service
android:name=".RemoteService"
android:process=":remote_service" />
</application>
</manifest>
One thing worth mentioning here is that, no matter whether the service is running in its own process, the code to send and receive messages stay the same.
And finally, we can bind to the service and send messages to it:
class MainActivity : Activity() {
private var messenger: Messenger? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// when the service is connected, we create the Messenger to send messages
messenger = Messenger(service)
// send a message
messenger?.send(Message.obtain().apply { what = 777 })
}
override fun onServiceDisconnected(name: ComponentName) {
// nullify the messenger in case the service is disconnected unexpectedly,
// e.g. the remote process crashes
messenger = null
}
}
override fun onStart() {
super.onStart()
// bind the service, and start it if needed
bindService(Intent(this, RemoteService::class.java), connection, BIND_AUTO_CREATE)
}
override fun onStop() {
// disconnect from the service, and nullify the messenger
unbindService(connection)
messenger = null
super.onStop()
}
}
Conclusion
Hopefully, this article can give you some idea on how some low-level Android components work.
For most of the time, we probably don’t need to directly work with them, but it’s important and interesting to know how the platform works, and what tools we can use if needed.