从 AsyncTask 说起

Android网络库之HttpURLConnection与数据解析我们提到了利用 AsyncTask 在后台运行耗时操作。

AsyncTask是执行后台线程的最简单方式,但它不适用于那些重复且长时间运行的任务。虽然耗时操作在后台运行,不会使系统崩溃,但是我们如果加载图片不能等它全都下载之后一起显示(内存占用也是一个问题)。

考虑到上面的问题,很多应用通常会选择仅在需要显示图片时才需要下载。

主线程

Android系统中,线程使用的收件箱叫作消息队列(message queue)。使用消息队列的线程叫作消息循环(message loop)。消息循环会循环检查队列上是否有新消息。消息循环由线程和looper组成。Looper对象管理着线程的消息队列。主线程就是个消息循环,因此也拥有looper。主线程的所有工作都是由其looper完成的。looper不断从消息队列中抓取消息,然后完成消息指定的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private ThumbnailDownloader<PhotoHolder> mThumbnailDownloader;
...
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
new FetchItemsTask().execute();

// 创建 HandlerThread
mThumbnailDownloader = new ThumbnailDownloader<>();
// 初始化Looper
mThumbnailDownloader.start();
mThumbnailDownloader.getLooper();
Log.i(TAG, "Background thread started");
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
...
}

@Override
public void onDestroyView() {
super.onDestroyView();
mThumbnailDownloader.clearQueue();
}
  • ThumbnailDownloader的 getLooper() 方法是在 start() 方法之后调用的。(稍后会学习更多有关Looper的知识)这能保证线程就绪,避免潜在竞争(尽管极少发生)。因为 getLooper() 方法能执行成功,说明 onLooperPrepared() 方法肯定早已完成。这样,queueThumbnail()方法因Handler为空而调用失败的情况就能避免了。
  • onDestroy() 方法内调用 quit() 方法结束线程。这非常关键。如不终止 HandlerThread ,它会一直运行下去,成为僵尸。
1
2
3
4
5
6
7
8
@Override
public void onBindViewHolder(@NonNull PhotoHolder photoHolder, int position) {
GalleryItem galleryItem = mGalleryItems.get(position);
Drawable placeholder = getResources().getDrawable(R.drawable.bill_up_close);
photoHolder.bindDrawable(placeholder);
// 获取图片 url 传入,添加下载任务
mThumbnailDownloader.queueThumbnail(photoHolder, galleryItem.getUrl());
}

Handler、Looper、Message 结构分析

剖析Message

Message需要自己定义三个变量,分别是

  • What:用户定义的int型消息代码,用来描述消息
  • obj:用户指定,随消息发送的对象
  • target:处理消息的 Handler

剖析 Handler

结构图
结构图

作用:创建、发布、处理 Message

我们来说一下 Handler 使用时的流程,结合一下代码分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void queueThumbnail(T target, String url){
Log.i(TAG, "Got a URL: "+url);
if(url == null){
mRequestMap.remove(target);
}else{
// 使用一个标记下载请求的T类型作为key,
// 我们可以存取和请求关联的URL下载链接
// 这个标记对象是PhotoHolder, 下载结果就能很方便的发送给显示图片的UI元素
mRequestMap.put(target, url);
// 将下载消息添加到后台线程的消息队列中
mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD, target)
.sendToTarget();
}
}
  1. 我们在obtainMessage(…) 方法获取消息,紧接着用让这个Message调用 sendToTarget() 方法将其发送给它的Handler,Handler 会将这个Message放置在 Looper 消息队列的尾部。
  2. Looper 取得消息队列中的Message后,会将他发送给消息的目标Handler去处理。消息一般实在目标 Handler 的 Handler.handleMessage(…) 实现方法中进行处理的。
Handler创建、发送Message
Handler创建、发送Message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void onLooperPrepared() {
mRequestHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
if(msg.what == MESSAGE_DOWNLOAD){
T target = (T)msg.obj;
Log.i(TAG, "Got a request for URL: " + mRequestMap.get(target));
// 获取图片的耗时操作
handleRequest(target);
}
}
};
}

上述代码中,我们是在 onLooperPrepared() 方法里实现 Handler.handleMessage(...) 方法的。HandlerThread.onLooperPrepared() 是在Looper首次检查消息队列之前调用,所以该方法是创建Handler实现的好地方。

在Handler.handleMessage(…)方法中,首先检查消息类型(what),再获取obj值(T类型下载请求),然后将其传递给handleRequest(…)方法处理。(前面说过,队列中的下载消息取出并可以处理时,就会触发调用Handler.handleMessage(…)方法。handleRequest() 方法是下载执行的地方。在这里,确认URL有效后,就将它传递给FlickrFetchr新实例。确切地说,此处使用的是Android网络库之HttpURLConnection与数据解析中创建的FlickrFetchr.getUrlBytes(…)方法。

传递 Handler

我们使用 mRequestHandler,已经可以从主线程安排后台任务。

从主线程安排 ThumbnailDownloader 上的任务
从主线程安排 ThumbnailDownloader 上的任务

反过来,也可以从后台线程使用与主线程关联的Handler,安排主线任务

![从ThumbnailDownloader线程上规划主线程任务]

主线程是一个拥有 handler 和 Looper 的消息循环。主线程上创建的 Handler 会自动与它的 Looper 相关联。主线程上创建的这个 Handler 也可以传递给另一线程。传递出去的 Handler 与创建它的线程 Looper 始终保持着联系。因此,已传出 Handler 负责处理的所有消息都将在主线程的消息队列中处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// HandlerThread 文件
// 来自主线程的 Handler
private Handler mResponseHandler;
private ThumbnailDownloadListener<T> mThumbnailDownloadListener;

// 新增监听器接口 响应下载请求
public interface ThumbnailDownloadListener<T>{
void onThumbnailDownloaded(T target, Bitmap bitmap);
}

public void setThumbnailDownloadListener(ThumbnailDownloadListener<T> listener){
mThumbnailDownloadListener = listener;
}

public ThumbnailDownloader(Handler responseHandler){
super(TAG);
mResponseHandler = responseHandler;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 主线程创建的Handler
Handler responseHandler = new Handler();

mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler);
mThumbnailDownloader.setThumbnailDownloadListener(
new ThumbnailDownloader.ThumbnailDownloadListener<PhotoHolder>() {
// 实现接口中的抽象方法,来更新UI
@Override
public void onThumbnailDownloaded(PhotoHolder photoHolder, Bitmap bitmap) {
Drawable drawable = new BitmapDrawable(getResources(), bitmap);
photoHolder.bindDrawable(drawable);
}
}
);

那么就有一个疑问了,如果在这里面更新UI,那么在PhotoAdapter中的 onBindViewHolder() 中的

1
mThumbnailDownloader.queueThumbnail(photoHolder, galleryItem.getUrl());

是什么用途呢?

其实显而易见 queueThumbnail() 方法的作用是向消息队列中添加下载请求,而下载之后负责更新UI是利用在主线程创建的 mResponseHandler 处理的。

说回正题,现在,通过mResponseHandler,ThumbnailDownloader能够使用与主线程 Looper 绑定的 Handler。同时,还有ThumbnailDownloadListener 使用返回的 Bitmap 执行UI更新操作。具体来说,就是通过 onThumbnailDownloaded 实现,使用新下载的 Bitmap 来设置 PhotoHolder 的 Drawable。

我们在 handleRequest 方法中添加下面代码

1
2
3
4
5
6
7
8
9
10
mResponseHandler.post(new Runnable() {
@Override
public void run() {
if(mRequestMap.get(target) != url || mHasQuit){
return;
}
mRequestMap.remove(target);
mThumbnailDownloadListener.onThumbnailDownloaded(target, bitmap);
}
});

Message设有回调方法属性后,取出队列的消息是不会发给target Handler的。相反,存储在回调方法中的 Runnable 的 run() 方法会直接执行。

那么上述代码有什么作用呢?

  • 首先,它再次检查 requestMap。这很有必要,因为 RecyclerView 会循环使用其视图。在ThumbnailDownloader下载完成 Bitmap 之后,RecyclerView可能循环使用了 PhotoHolder 并相应请求了一个不同的URL。该检查可保证每个PhotoHolder都能获取到正确的图片,即使中间发生了其他请求也无妨。
  • 接下来,检查mHasQuit值。如果ThumbnailDownloader已经退出,运行任何回调方法可能都不太安全。
  • 最后,从requestMap中删除配对的PhotoHolder-URL,然后将位图设置到目标PhotoHolder上。

总结

最后,我们做一个流程总结,当然,很多细节我没有写在里面,例如mResponse的监听器接口、清理队列、处理完一个Handler之后在Map中删除相应的target,仅供参考。

流程总结
流程总结

之后会从源码的角度继续分析Android的Handler机制。