Looper, Handler in Android Part 4 - Handler
Understanding the flow of message using sendMessage() and handleMessage() method
Photo by Joshua Hoehne on Unsplash
This is the final article in our series on Introduction Looper and Handler in Android. To fully grasp the concepts discussed here, it's assumed that you have read or are familiar with the content covered in previous parts. If not, I recommend revisiting those articles first.
Recap of How to Use Handler
Handler can be utilized in two main ways:
Passing Runnable: Adding or posting operation to be executed directly inside the thread from another thread. This is accomplished using the
post()
method ofHandler
.Sending Message: Sending a message about an operation that needs to be done inside the thread from another thread. This involves a combination of the
sendMessage()
andhandleMessage()
methods ofHandler
.While we coverted the
post()
method extensively in part3, here we will delve into thesendMessage()
andhandleMessage()
methods of theHandler
.
When to use sendMessage()
As the name suggests, sendMessage()
is used to send a Message
object to a Handler
and handleMessage()
will perform certain operation on the received Message
. The two methods, sendMessage()
and handleMessage()
, must always be used in conjunction. If you usesendMessage()
to send a Message
to a Handler
, the Handler
must also implement its handleMessage()
to process that message.
Unlike post()
, which is suitable for executing a block of code, sendMessage()
is used for reusable pieces of code that need to be handled frequently within your thread. For example, when multiple parts of your code needs to save documents, you can use sendMessage()
to organize your code more efficiently. All the logic related to file operations will be handled separately, enabling reuse, concurrency, separation of concerns, and synchronous access to the file I/O operations. By passing the path as an argument, the same code can be utilized for multiple other documents, further enhancing its flexibility and maintainability.
Note: In terms of synchronicity, The
sendMessage()
call itself is asynchronous, non-blocking and can be executed from any thread. ThehandleMessage()
method associated with it will always be invoked synchronously and will run on the same thread that the Handler operates on.
Internals of sendMessage()
Internally both post()
and sendMessage()
work similarly. The difference lies in how they are handled as seen above. Typically, post()
is used for running a one-off block of code in another thread (e.g., updating the UI), and sendMessage()
is used for operations that need to be repeated. However, there is no strict rule in the language construct, and it largely depends on your use case and programming style.
To reiterate, here's an excerpt from the Android documentation of Handler
The post versions allow you to enqueue Runnable objects to be called by the message queue when they are received; the sendMessage versions allow you to enqueue a Message object containing a bundle of data that will be processed by the Handler's handleMessage(Message) method (requiring that you implement a subclass of Handler).
Similar to post()
, sendMessage()
also calls sendMessageDelayed()
function to add the Message
to the MessageQueue
(refer to part 3 for the detailed flow of post()
). The only difference lies in the dispatchMessage
method which calls handleMessage()
instead.
// SajalRG: adds message to the queue, called from any thread
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
/**
* Handle system messages here.
*/
// SajalRG: executed from Looper.loop() inside the thread represented by handler
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
Example from AOSP SystemUI
Let's examine an example from AOSP on how this pair can be utilized. We will take a look at the CommandQueue
class.
Note: Our objective here is not to delve into the intricacies of the
CommandQueue
class itself. Instead, we aim to focus solely on how it leverages thesendMessage()
andhandleMessage()
methods. This approach provides insight into real-world usage in larger projects, offering a practical understanding rather than relying on contrived examples.
/**
* This class takes the functions from IStatusBar that come in on
* binder pool threads and posts messages to get them onto the main
* thread, and ...
*/
public class CommandQueue extends IStatusBar.Stub implements
CallbackController<Callbacks> {
In this example, CommandQueue
also extends
the IStatusBar.stub()
, which we can ignore for now but it could be a topic for furture articles on Binder
and AIDL
. CallbackController
is an interface
which the CommandQueue
implements, but we won't explore it further in this post.
For our purpose, CommandQueue
has some methods that are called from other threads, referred to as binder pool threads
, and it will post the messages onto the main thread.
Understanding CommandQueue
If you view the CommandQueue
class from this link, you might notice that there is no sendMessage()
call in the class, even though we are using this as an example for sendMessage()
.
To understand this, let's look at Message
class. We partially covered the Message
class in part 2, please review if you haven't already. Now, let's introduce method sendToTarget
class which is used here. It internally calls the sendMessage()
for us. Both approaches are okay.
/**
* Sends this Message to the Handler specified by {@link #getTarget}.
* Throws a null pointer exception if this field has not been set.
*/
public void sendToTarget() {
target.sendMessage(this);
}
Also, instead of using Message.obtain()
to create new Message
, as we saw in part3, Handler.obtainMessage()
is used here, which internally does the same. Below is the excerpt from the Handler
class listing all other possible ways to obtain Message
.
@NonNull
public final Message obtainMessage() {
return Message.obtain(this);
}
/**
* Same as {@link #obtainMessage()}, except that it also sets the what member of the returned Message.
*
* @param what Value to assign to the returned Message.what field.
* @return A Message from the global message pool.
*/
@NonNull
public final Message obtainMessage(int what) {
return Message.obtain(this, what);
}
/**
*
* Same as {@link #obtainMessage()}, except that it also sets the what and obj members
* of the returned Message.
*
* @param what Value to assign to the returned Message.what field.
* @param obj Value to assign to the returned Message.obj field.
* @return A Message from the global message pool.
*/
@NonNull
public final Message obtainMessage(int what, @Nullable Object obj) {
return Message.obtain(this, what, obj);
}
/**
*
* Same as {@link #obtainMessage()}, except that it also sets the what, arg1 and arg2 members of the returned
* Message.
* @param what Value to assign to the returned Message.what field.
* @param arg1 Value to assign to the returned Message.arg1 field.
* @param arg2 Value to assign to the returned Message.arg2 field.
* @return A Message from the global message pool.
*/
@NonNull
public final Message obtainMessage(int what, int arg1, int arg2) {
return Message.obtain(this, what, arg1, arg2);
}
/**
*
* Same as {@link #obtainMessage()}, except that it also sets the what, obj, arg1,and arg2 values on the
* returned Message.
* @param what Value to assign to the returned Message.what field.
* @param arg1 Value to assign to the returned Message.arg1 field.
* @param arg2 Value to assign to the returned Message.arg2 field.
* @param obj Value to assign to the returned Message.obj field.
* @return A Message from the global message pool.
*/
@NonNull
public final Message obtainMessage(int what, int arg1, int arg2, @Nullable Object obj) {
return Message.obtain(this, what, arg1, arg2, obj);
}
In above code snippet, different signature which allows us to pass additional information to the Message
are also listed. Here, what
is primarily used in handleMessage()
identify which operation to perform.
Message flow in CommandQueue
Let's explore the addQsTile
method to explore the message flow. First, the method is called from the binder thread.
// SajalRG: create new handler using the main looper
private Handler mHandler = new H(Looper.getMainLooper());
@Override
public void addQsTile(ComponentName tile) {
synchronized (mLock) {
// SajalRG: get message object and send the message
mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget();
}
}
Then, it is handled inside the Handler
private final class H extends Handler {
private H(Looper l) {
super(l);
}
// SajalRG: handleMessage is called in conjunction to the sendMessage
public void handleMessage(Message msg) {
final int what = msg.what & MSG_MASK;
// SajalRG: what is used to identify which operation to execute
switch (what) {
// SajalRG: other cases
case MSG_ADD_QS_TILE:
// SajalRG: actual operation happening inside the main thread
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).addQsTile((ComponentName) msg.obj);
}
break;
// SajalRG: other cases
}
}
}
What's next
This post concludes our introduction to Handler
, Looper
and Thread
. I hope you have a clearer understanding of the subject and how they are interconnected at a high level. I'd still like to continue this series addressing some misconceptions I'd come across and answer some frequently asked questions related to this topic. If you have any specific topics or query you'd like me to cover, please feel free to leave a comment.
Your support serves as great encouragement for me to continue creating and sharing content.