您的位置:首页 > 移动开发 > Android开发

Android: Looper, Handler, HandlerThread. Part II

2015-09-08 17:50 323 查看
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
ofMessages, 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
and
arg2
are
lower-cost alternatives to using
setData()
if you only need to store
a few integer values.
public int arg2
:
arg1
and
arg2
are
lower-cost alternatives to using
setData()
if you only need to store
a few integer values.
public Object obj
: An arbitrary object to
send to the recipient.
public Messenger replyTo
: Optional
Messenger
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 call
Message.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 the
target
field), we should provide it explicitly
(or you can call
setTarget
later). Also you can attach a
Bundle
with
Parcelable
types
by calling
setData
. However, if we are going to obtain messages from
the
Handler
, it has a family of shorthand methods:
obtainMessage
.
They look almost identical to
Message.obtain
methods, but without
Handler
argument,
current instance of
Handler
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 any
Runnable
you
want to run when
Message
will be processed (it is the same
Runnable
we
have used in the previous part to post tasks to the
MessageQueue
, we
will get back to them later).

How can I send message? You have 2 choices here:

you can call
sendToTarget
method on your
Message
instance,
message will be placed at the end of
MessageQueue
.

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
from
MessageQueue
are
going to
dispatchMessage
method of the
Handler
instance
specified in
message.target
field. Once
Handler
gets
message at the
dispatchMessage
it checks whether
message.callback
field
is
null
or not. If it's not
null
message.callback.run()
will
be called, otherwise message will be passed to
handleMessage
method.
By default, this method has an empty body at the
Handler
class, therefore
you should either extend
Handler
class and override this method or you
can provide an implementation of
Handler.Callback
interface at the
Handler
constructor
call. This interface has only one method you should write -
handleMessage
.
Now it is clear, that when we used
handler.post*
methods at the previous
part, we actually created messages with
callback
field set to our
Runnable
.

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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: