Android: Looper, Handler, HandlerThread. Part II.
In the previous part I've covered basic interaction in a bundle Handler
+Looper
+HandlerThread
. The significant part under the hood of this team was MessageQueue
with tasks represented by Runnables
. This is very straightforward approach, which is used to simplify user's life. But in reality MessageQueue
consists of Messages, not the Runnables
. Let's look on this class closer.
Official documentation says the following regarding the Message
class description:
Defines a message containing a description and arbitrary data object that can be sent to a Handler. This object contains two extra int fields and an extra object field that allow you to not do allocations in many cases.
We are very interested in these "extra" fields. Here they are according to documentation:
public int arg1
:arg1
andarg2
are lower-cost alternatives to usingsetData()
if you only need to store a few integer values.public int arg2
:arg1
andarg2
are lower-cost alternatives to usingsetData()
if you only need to store a few integer values.public Object obj
: An arbitrary object to send to the recipient.public Messenger replyTo
: OptionalMessenger
where replies to this message can be sent.public int what
: User-defined message code so that the recipient can identify what this message is about.
Not very clear how to use them, right? But the most interesting fields are hidden inside the class with package level access, here they are:
int flags
long when
Bundle data
Handler target
Runnable callback
If this is a message, then you should ask yourself the following questions: How can I get a message? How should I fill it? How can I send it? How it will be processed? Let's try to answer on these questions:
-
How can I get a message? Since every message represent the task we need to process, you may need many messages. Eventually, instead of creating a new
Message
object for each task, you can reuse messages from the pool, it's much cheaper. To do that, just callMessage.obtain
. -
How should I fill it? There are several overloaded variants of
Message.obtain
where you can provide data you want (or copy data from another message):
-
obtain(Handler h, int what, int arg1, int arg2)
-
obtain(Handler h, Runnable callback)
-
obtain(Handler h)
-
obtain(Handler h, int what)
-
obtain(Handler h, int what, Object obj)
-
obtain(Handler h, int what, int arg1, int arg2, Object obj)
-
obtain(Message orig)
If we want our message to be associated with specific
Handler
(which will be written to thetarget
field), we should provide it explicitly (or you can callsetTarget
later). Also you can attach aBundle
withParcelable
types by callingsetData
. However, if we are going to obtain messages from theHandler
, it has a family of shorthand methods:obtainMessage
. They look almost identical toMessage.obtain
methods, but withoutHandler
argument, current instance ofHandler
will be provided automatically.what
field is used to identify a type of message,obj
is used to store some useful object you want to attach to the message,callback
is anyRunnable
you want to run whenMessage
will be processed (it is the sameRunnable
we have used in the previous part to post tasks to theMessageQueue
, we will get back to them later).
- How can I send message? You have 2 choices here:
-
you can call
sendToTarget
method on yourMessage
instance, message will be placed at the end ofMessageQueue
. -
you can call one of the following methods on your
Handler
instance providing message as an argument:sendMessageAtFrontOfQueue
sendMessageAtTime
sendMessageDelayed
sendMessage
- How it will be processed? Messages taken by the
Looper
fromMessageQueue
are going todispatchMessage
method of theHandler
instance specified inmessage.target
field. OnceHandler
gets message at thedispatchMessage
it checks whethermessage.callback
field isnull
or not. If it's notnull
message.callback.run()
will be called, otherwise message will be passed tohandleMessage
method. By default, this method has an empty body at theHandler
class, therefore you should either extendHandler
class and override this method or you can provide an implementation ofHandler.Callback
interface at theHandler
constructor call. This interface has only one method you should write -handleMessage
. Now it is clear, that when we usedhandler.post*
methods at the previous part, we actually created messages withcallback
field set to ourRunnable
.
Ok, we are done with theory, now it's time to make something useful. Like at the previous part we still have a layout with progress bar as an indicator of non-blocking UI execution, but now we will add two vertical LinearLayouts
with equal widths (both occupy half or the screen) to host ImageViews
:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:gravity="center">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:id="@+id/leftSideLayout">
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1"
android:id="@+id/rightSideLayout">
</LinearLayout>
</LinearLayout>
</LinearLayout>
And here is a code of MyActivity.java
we will be using for test:
public class MyActivity extends Activity
implements MyWorkerThread.Callback {
private static boolean isVisible;
public static final int LEFT_SIDE = 0;
public static final int RIGHT_SIDE = 1;
private LinearLayout mLeftSideLayout;
private LinearLayout mRightSideLayout;
private MyWorkerThread mWorkerThread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
isVisible = true;
mLeftSideLayout = (LinearLayout) findViewById(R.id.leftSideLayout);
mRightSideLayout = (LinearLayout) findViewById(R.id.rightSideLayout);
String[] urls = new String[]{"http://developer.android.com/design/media/principles_delight.png",
"http://developer.android.com/design/media/principles_real_objects.png",
"http://developer.android.com/design/media/principles_make_it_mine.png",
"http://developer.android.com/design/media/principles_get_to_know_me.png"};
mWorkerThread = new MyWorkerThread(new Handler(), this);
mWorkerThread.start();
mWorkerThread.prepareHandler();
Random random = new Random();
for (String url : urls){
mWorkerThread.queueTask(url, random.nextInt(2), new ImageView(this));
}
}
@Override
protected void onPause() {
isVisible = false;
super.onPause();
}
@Override
protected void onDestroy() {
mWorkerThread.quit();
super.onDestroy();
}
@Override
public void onImageDownloaded(ImageView imageView, Bitmap bitmap, int side) {
imageView.setImageBitmap(bitmap);
if (isVisible && side == LEFT_SIDE){
mLeftSideLayout.addView(imageView);
} else if (isVisible && side == RIGHT_SIDE){
mRightSideLayout.addView(imageView);
}
}
}
And finally MyWorkerThread.java
:
public class MyWorkerThread extends HandlerThread {
private Handler mWorkerHandler;
private Handler mResponseHandler;
private static final String TAG = MyWorkerThread.class.getSimpleName();
private Map<ImageView, String> mRequestMap = new HashMap<ImageView, String>();
private Callback mCallback;
public interface Callback {
public void onImageDownloaded(ImageView imageView, Bitmap bitmap, int side);
}
public MyWorkerThread(Handler responseHandler, Callback callback) {
super(TAG);
mResponseHandler = responseHandler;
mCallback = callback;
}
public void queueTask(String url, int side, ImageView imageView) {
mRequestMap.put(imageView, url);
Log.i(TAG, url + " added to the queue");
mWorkerHandler.obtainMessage(side, imageView)
.sendToTarget();
}
public void prepareHandler() {
mWorkerHandler = new Handler(getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
ImageView imageView = (ImageView) msg.obj;
String side = msg.what == MyActivity.LEFT_SIDE ? "left side" : "right side";
Log.i(TAG, String.format("Processing %s, %s", mRequestMap.get(imageView), side));
handleRequest(imageView, msg.what);
msg.recycle();
return true;
}
});
}
private void handleRequest(final ImageView imageView, final int side) {
String url = mRequestMap.get(imageView);
try {
HttpURLConnection connection =
(HttpURLConnection) new URL(url).openConnection();
final Bitmap bitmap = BitmapFactory
.decodeStream((InputStream) connection.getContent());
mRequestMap.remove(imageView);
mResponseHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onImageDownloaded(imageView, bitmap, side);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
What does this code do? It loads 4 images from http://developer.android.com and puts its either to the left or right LinearLayout
randomly. I'll skip views initialization and go to the interesting part:
String[] urls = new String[]{"http://developer.android.com/design/media/principles_delight.png",
"http://developer.android.com/design/media/principles_real_objects.png",
"http://developer.android.com/design/media/principles_make_it_mine.png",
"http://developer.android.com/design/media/principles_get_to_know_me.png"};
mWorkerThread = new MyWorkerThread("myWorkerThread", new Handler(), this);
mWorkerThread.start();
mWorkerThread.prepareHandler();
Random random = new Random();
for (String url : urls){
mWorkerThread.queueTask(url, random.nextInt(2), new ImageView(this));
}
At the code above I created a new instance of MyWorkerThread
by providing a Handler
which will be used for posting results to the UI thread (it is implicitly tied to UI thread as I said in previous part) and a callback (which is implemented by our activity instead of creating stand-alone object for it). Callback is represented by the following simple interface and its purpose is to do the necessary UI updates:
public static interface Callback {
public void onImageDownloaded(ImageView imageView, Bitmap bitmap, int side);
}
And that's it for activity, we delegated the task of loading images to another thread. Now it's turn of HandlerThread
. Nothing interesting in constructor, we just save the necessary objects, lets take a look on the queueTask
method:
public void queueTask(String url, int side, ImageView imageView) {
mRequestMap.put(imageView, url);
Log.i(TAG, url + " added to the queue");
mWorkerHandler.obtainMessage(side, imageView)
.sendToTarget();
}
We are adding ImageView
and URL to the request map here and create a message with message.target
field set to mWorkerHandler
by calling its obtainMessage
method, also we set message.obj
to imageView
and message.what
to the value of side
argument. After that the message is sent to the end of MessageQueue
, now we can take a look on handling message once it is pulled from MessageQueue
, the necessary processing was written at the worker Handler
initialization at the prepareHandler
method:
public void prepareHandler() {
mWorkerHandler = new Handler(getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
ImageView imageView = (ImageView) msg.obj;
String side = msg.what == MyActivity.LEFT_SIDE ? "left side" : "right side";
Log.i(TAG, String.format("Processing %s, %s", mRequestMap.get(imageView), side));
handleRequest(imageView, msg.what);
msg.recycle();
return true;
}
});
}
Instead of sub-classing Handler
to make my own implementation of handleMessage
method, I've used Handler.Callback
interface, 2 seconds delay was added to emulate the delay in handling images. All we need to do is just to extract the necessary data from the message and pass it to our processing method - handleRequest
:
private void handleRequest(final ImageView imageView, final int side) {
String url = mRequestMap.get(imageView);
try {
HttpURLConnection connection =
(HttpURLConnection) new URL(url).openConnection();
final Bitmap bitmap = BitmapFactory
.decodeStream((InputStream) connection.getContent());
mRequestMap.remove(imageView);
mResponseHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onImageDownloaded(imageView, bitmap, side);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
It loads the necessary bitmap and once we are done we can remove this item from request map and call a callback which will be executed on the UI
thread. That's it, nothing complex. Now we have a background sequential worker which is tied to the activity's lifecycle.