Android编程研究(二)——Message和message Handler

来源:互联网 发布:老子说的知之者 编辑:程序博客网 时间:2024/06/06 19:24

前言

上次我们讲到了Android里面的AsyncTask以及它的用法。AsyncTask作为Android特有的一个异步类,对于简单的异步操作来说是非常方便的。但是对于一些操作比较多,或者是十分耗时的,长连接之类的,我们就无法使用AsyncTask来处理这些问题,这时候第二个法宝就来了:Message和message Handler。

介绍

消息

在进入下面的学习之前,我们需要先了解一下什么是Message,以及它和Handler(或者说是message handler)之间的关系。

消息是Message类的一个实例,包含有好多个实例变量。其中三个需要在实现时定义:

what 用户定义的int型消息代码,用来描述消息;

obj 随消息发送的用户指定对象;

target 处理消息的Handler。

Message的目标是Handler类的一个实例。Handler可看作是“message handler”的简称。Message在创建时,会自动与一个Handler相关联。Message在准备处理状态下,Handler是负责让消息处理行为发生的对象。

Handler

要处理消息以及消息指定的任务,首先需要一个消息Handler实例。Handler不仅仅是处理Message的目标(target),也是创建和发布Message的接口。

Looper拥有Message对象的收件箱,所以Message必须在Looper上发布或读取。基于Looper和Message的这种关系,为与Looper协同工作,Handler总是引用着它。

一个Handler仅与一个Looper相关联,一个Message也仅与一个目标Handler(也称作Message目标)相关联。Looper拥有整个Message队列。多个Handler可与一个Looper相关联,这意味着一个Handler的Message可能与另一个Handler的Message存放在同一个消息队列中。

使用Handler

消息的目标Handler通常不需要手动设置。一个比较理想的方式是,我们调用Handler.obtainMessage(...)方法创建消息并传入其他消息字段,然后该方法自动完成目标Handler的设置。

为了避免创建新的Message对象,Handler.obtainMessage(...)方法会从公共循环池里获取消息。因此相比创建新实例,这样有效率多了。

一旦取得Message,我们就调用sendToTarget()方法将其发送给它的Handler。紧接着Handler会将Message放置在Looper消息队列的尾部。

Looper取得消息队列中的特定消息后,会将它发送给消息目标去处理。消息一般是在目标的Handler.handleMessage(...)方法实现中进行处理的。

下面以一个通过url下载图片字节再转换成位图的例子来讲一下,先看一下代码:

public class ThumbnailDownloader<Token> extends HandlerThread {private static final String TAG = "ThumbnailDownloader";private static final int MESSAGE_DOWNLOAD = 0;Handler mHandler;Map<Token, String> requestMap = Collections.synchronizedMap(new HashMap<Token, String>());//3Handler mResponseHandler;//7Listener<Token> mListener;public interface Listener<Token> {void onThumbnailDownloaded(Token token, Bitmap thumbnail);}public void setListener(Listener<Token> listener) {mListener = listener;}public ThumbnailDownloader(Handler responseHandler) {super(TAG);mResponseHandler = responseHandler;}@SuppressLint("HandlerLeak")//1@Overrideprotected void onLooperPrepared() {mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {//5if (msg.what == MESSAGE_DOWNLOAD) {@SuppressWarnings("unchecked")//2Token token = (Token) msg.obj;Log.i(TAG,"Got a request for url: " + requestMap.get(token));handleRequest(token);}}};}public void queueThumbnail(Token token, String url) {Log.i(TAG, "Got an URL: " + url);requestMap.put(token, url);//4mHandler.obtainMessage(MESSAGE_DOWNLOAD, token).sendToTarget();}public void clearQueue() {//10mHandler.removeMessages(MESSAGE_DOWNLOAD);requestMap.clear();}private void handleRequest(final Token token) {try {final String url = requestMap.get(token);if (url == null)return;byte[] bitmapBytes = new FlickrFetchr().getUrlBytes(url);//6final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0,bitmapBytes.length);Log.i(TAG, "Bitmap created");mResponseHandler.post(new Runnable() {//9public void run() {if (requestMap.get(token) != url)return;requestMap.remove(token);mListener.onThumbnailDownloaded(token, bitmap);}});} catch (IOException ioe) {Log.e(TAG, "Error downloading image", ioe);}}}
//1 首先先说一下onLooperPreprepared()方法上面的@SuppressLint("HandlerLeak")注解。这里,Android Lint将报出Handler类相关的警告信息。Looper控制着Handler的生死,因此如果Handler是匿名内部类,则隐式的对象引用很容易导致内存泄漏。不过,所有对象都与HandlerThread绑定在一起,因此这里不用担心任何内存泄漏的问题。

//2 至于了里面还有一个@SuppressWarnings("unchecked")的注解,是常见的普通注解。这里必须使用该注解,因为Token是泛型类参数,而Message.obj是一个Object。由于类型擦除,这里的强制类型转换是不行的。

//3 变量requestMap是一个同步HashMap。这里,使用Token作为key,可以存储或获取与特定Token相关连的URL。

//4 在queueThumbnail()方法中,将传入的Token-URL键值对放入map中。然后以Token为obj获取一条信息,并发送出去以存放到消息队列中。

//5 在onLooperPrepared()方法中,我们在Handler子类中实现了Handler.handleMessage(...)方法。因为HandlerThread。onooperPrepared()方法的调用发生在Looper第一次检查消息队列之前,所以该方法成了我们创建Handler实现的好地方。

在Handler。handleMessage(...)方法中,检查消息类型,获取Token,然后将其传递给handleRequest(...)方法。

//6 handleMessage(...)方法是下载动作发生的地方。我们可以在这里做一些消息传递的处理,例如上面的FlickrFetchr.getUrlBytes(...)方法。再用BitmapFactory将getUrlBytes(...)返回的字节数组转换为位图。

传递handler

HandlerThread能在主线程上完成任务的一种方式是,让主线程将其自身的Handler传递给HandlerThread。

主线程是一个拥有handler和Looper的消息循环。主线程上创建的Handler会自动与它的Looper相关联。我们可以将主线程上创建的Handler传递给另一个线程。传递出去的Handler与创建它的线程Looper始终保持着联系。因此,任何已经传出去的Handler负责处理的消息都将在主线程的消息队列中处理。这看上去就像我们在使用ThumbnailDownloader的Handler,实现在主线程上安排的后台线程上的任务。反过来,我们也可以从后台线程使用与主线程关联的Handler,安排要在主线程上完成的任务。

//7 在上面的ThumbnailDownloader中,变量mResponseHandler用来存放主线程的Handler。然后以一个接受Handler的构造方法替换原有构造方法,并设置变量的值,最后新增一个用来通信的监听器接口。

//8 修改你的UI线程所在的类,将Handler传递给ThumbnailDownloader,并设置Listener,将返回的BitMap设置给ImageView。记住,Handler默认与当前线程的Looper相关联。该Handler是在onCreate(...)方法中创建的,因此它将与主线程的Looper相关联。

@Overridepublic void onCreate(Bundle savedInstanceState) {<span style="white-space:pre"></span>...<span style="white-space:pre"></span>mThumbnailThread = new ThumbnailDownloader<ImageView>(new Handler());//8<span style="white-space:pre"></span>mThumbnailThread<span style="white-space:pre"></span>.setListener(new ThumbnailDownloader.Listener<ImageView>() {<span style="white-space:pre"></span>public void onThumbnailDownloaded(ImageView imageView,<span style="white-space:pre"></span>Bitmap thumbnail) {<span style="white-space:pre"></span>if (isVisible()) {<span style="white-space:pre"></span>imageView.setImageBitmap(thumbnail);<span style="white-space:pre"></span>}<span style="white-space:pre"></span>}<span style="white-space:pre"></span>});<span style="white-space:pre"></span>mThumbnailThread.start();<span style="white-space:pre"></span>mThumbnailThread.getLooper();<span style="white-space:pre"></span>Log.i(TAG, "Background thread started");}

通过mResponseHandler,ThumbnailDownloader能够访问与主线程Looper绑定的Handler。同时,它的Listener会使用返回的Bitmap执行UI更新操作。在调用imageView.setImageBitmap(Bitmap)方法之前,应首先调用Fragment.isVisible()检查方法,以保证不会将图片设置到无效的ImageView视图上去。

我们也可以返回定制Message到主线程上。不过这需要用到另一个Handler子类,以及一个handleMessage(...)覆盖方法。这里,我们使用的是另一种方便的Handler方法——post(Runable)。Handler.post(Runable)是一个张贴Message的便利方法。具体使用如下:

Runnable myRunnable = new Runable(){<span style="white-space:pre"></span>public void run(){<span style="white-space:pre"></span>/*Your code*/<span style="white-space:pre"></span>}};
//9 Message具有回调方法时,使用回调方法中的Runnable,而非其Handler目标来实现运行。因为mResponseHandler与主线程的Looper相关联,所以UI更新代码也是在主线程中完成的。

刚才的//9 的代码再次检查了一遍requestMap,这是因为GridView会循环使用它的视图。这个检查可以保证每个Token都能获得正确的图片,即使中间发生其他请求也没有关系。最后从requestMap中删除Token,然后将bitmap设置到Token上。

//10 如果用户旋转屏幕,因为ImageView视图的失效,ThumbnailDownloader则有可能会挂起。如果点击这些ImageView,就可能发生异常,所以要有清理队列外的所有请求。

既然视图已经销毁,那么在UI视图上也要有下载器清除代码

@Overridepublic void onDestroyView(){<span style="white-space:pre"></span>super.onDestroyView();<span style="white-space:pre"></span>mThumbnailThread.clearQueue();}
这一套流程也就到这里结束了,大家可以对比一下代码,看看它们是怎么联系起来的。

AsyncTask与Thread

看篇幅就知道AsyncTask比Handler和Looper简单很多,但是因为总总原因,下载图片这种场景还是不用AsyncTask。一个是因为AsyncTask的工作方式主要用于那些短暂且较多重复的任务。如果创建了大量的AsyncTask,或者长时间运行了AsyncTask,那么很可能是做了错误的选择。

在Android3.2版本起,AsyncTask不再为没一个AsyncTask实例单独创建一个线程。相反,它使用一个Executor在单一的后台线程上运行所有AsyncTask的后台服务。这意味着每一个AsyncTask都需要排队一个个来运行。那么长时间运行的AsyncTask就会把后面的AsyncTask阻塞。使用一个线程池executor虽然可以安全地并发运行多个AsyncTask,但不推荐这么做。如果真的要这么做,最好自己处理线程相关的工作,必要时使用Handler与主线程通信。




0 0
原创粉丝点击