AsyncTask是执行后台线程的最简单方式,但它不适用于那些重复且长时间运行的任务。
创建一个专用的后台线程,替换AsyncTask,这是实现按需下载的最常用方式。
Android系统中,线程使用的收件箱叫消息队列(message queue),使用消息队列的线程叫做消息循环(message loop),消息循环会循环检查队列上是否有新消息。
消息循环由线程和looper组成,Looper对象管理着线程的消息队列。
主线程就是个消息循环,因此也拥有looper。主线程的所有工作都是由其looper完成的。looper不断从消息队列中抓取消息,然后完成消息指定的任务。
创建一个同样是消息循环的后台线程。准备需要的looper时,会使用名为HandlerThread的类。
创建一个ThumbnailDownloader的新类,继承HandlerThread类,然后添加一个构造方法以及一个名为queueThumbnail()的存根方法,代码如下:
public class ThumbnailDownloader extends HandlerThread {
private static final String TAG="ThubnailDownloader";
private Boolean mHasQuit=false;
public ThumbnailDownloader(){
super(TAG);
}
@Override
public boolean quit() {
mHasQuit=true;
return super.quit();
}
public void queueThumbnail(T target,String url){
Log.i(TAG,"Got a URL: "+url);
}
}
ThumbnailDownloader类使用
为fragment类添加一个ThumbnailDownloader类型的成员变量,然后,在onCreate(...)方法中,创建并启动线程。最后,覆盖onDestroy()方法退出线程,代码如下:
public class PhotoGalleryFragment extends Fragment{
......
private ThumbnailDownloader mThumbnailDownloader;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
new FetchItemsTask().execute();
mThumbnailDownloader=new ThumbnailDownloader<>();
mThumbnailDownloader.start();
mThumbnailDownloader.getLooper();
Log.i(TAG,"Background thread started");
}
@Override
public void onDestroy() {
super.onDestroy();
mThumbnailDownloader.quit();
Log.i(TAG,"Background thread destroyed");
}
}
ThumbnailDownloader的泛型参数支持任何对象,这里,PhotoHolder是最终显示下载图片的地方,因此该视图最合适。
两点安全注意事项:
(1)在ThumbnailDownloader线程上,在start()方法之后调用getLooper()方法,这是一种保证线程就绪的处理方式,可以避免潜在竞争,调用getLooper()方法之前,没办法保证onLooperPrepared()方法已得到调用,所以Handler为空的话,调用queueThumbnail()方法很可能会失败。
(2)在onDestroy()方法内调用quit()方法结束线程,这十分重要,如不终止HandlerThread,它会一直运行下去。
最后在PhotoAdapter.onBindViewHolder(...)方法中,调用线程的queueThumbnail()方法,并传入参数。
public class PhotoGalleryFragment extends Fragment{
...
private class PhotoAdapter extends RecyclerView.Adapter{
...
@Override
public void onBindViewHolder(PhotoHolder holder, int position) {
GalleryItem galleryItem=mGalleryItems.get(position);
Drawable placeholder=getResources().getDrawable(R.drawable.girl);
holder.bindDrawable(placeholder);
mThumbnailDownloader.queueThumbnail(holder,galleryItem.getUrl());
}
...
}
}
消息是Message类的一个实例,它有好几个实例变量,其中有三个需在实现时定义:
What 用户定义的int型消息代码,用来描述信息;obj 随消息发送的用户指定对象;target 处理消息的Handler
Message的目标是Handler类的一个实例,Handler可看作是message handler的简称。创建Message时,它会自动与一个Handler相关联。Message待处理时,Handler对象负责触发消息处理事件。
要处理消息以及消息指定的任务,首先需要一个Handler实例。Handler不仅仅是处理Message的目标(target),也是创建和发布Message的接口。
Looper拥有Message收件箱,所以Message必须在Looper上发布或处理。由于存在着这种关系,为与Looper协同工作,Handler总是引用着它。
一个Handler仅与一个Looper相关联,一个Message也仅与一个目标Handler相关联,如下图。Looper拥有整个Message队列,多个Message可以引用同一目标Handler。
通常不需要手动设置消息的目标Handler。创建消息时,最好调用Handler.obtainMessage(...)方法。当传入其他消息字段给它时,该方法会自动设置目标给Handler对象。为避免创建新的Message对象,Handler.obtainMessage(...)方法会从公共循环池中获取消息,相比创建实例这样更有效率。
一旦取得Message,就可以调用sendToTarget()方法将其发送给它的Handler,然后Handler会将这个Message放置在Looper消息队列的尾部。Looper取得消息队列中的特定消息后,会将它发送给消息目标去处理,消息一般是在目标的Handler.handleMessage(...)实现方法中进行处理的。
其中的对象关系如下图:
对于PhotoGallery应用,我们会在queueThumbnail()实现方法中获取并发送消息给它的目标,消息的what属性是一个定义为MESSAGE_DOWNLOAD的常量,消息的obj属性是一个T类型对象,这里指PhotoHolder,它用于标识下载。
首先ThumbnailDownloader.java中添加一些常量和成员变量:
public class ThumbnailDownloader extends HandlerThread {
private static final String TAG="ThubnailDownloader";
private static final int MESSAGE_DOWNLOAD=0;
private Boolean mHasQuit=false;
private Handler mRequestHandler;
private ConcurrentMap mRequestMap=new ConcurrentHashMap<>();
...
}
mRequestHandler用来存储对Handler的引用。这个Handler负责在ThumbnailDownloader后台线程上管理下载请求消息队列,也负责从消息队列里取出并处理下载请求消息。
mRequestMap是个ConcurrentHashMap。这是一种线程安全的HashMap,这里使用一个标记下载请求的T类型对象作为key,可以存取和请求关联的URL下载链接。
接下来,在queueThumbnail(...)方法中添加代码,更新mRequestMap并把下载消息放到后台线程的消息队列中去:
public class ThumbnailDownloader extends HandlerThread {
...
public void queueThumbnail(T target,String url){
Log.i(TAG,"Got a URL: "+url);
if(url==null){
mRequestMap.remove(target);
}
else{
mRequestMap.put(target,url);
mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD,target).sendToTarget();
}
}
...
}
从mRequestHandler直接获取到消息后,mRequestHandler也就自动成为了这个新Message对象的target。这表明mRequestHandler会负责处理刚从消息队列中取出的这个消息。消息的what属性是MESSAGE_DOWNLOAD,它的obj属性是传递给queueThumbnail(...)方法的T target值。新消息就代表指定为T target的下载请求。
注意,消息自身是不带URL信息的。在该项目中的做法是使用PhotoHolder和URL的对应关系更新mRequestMap,随后从mRequestMap中取出图片URL,以保证总是使用了匹配PhotoHolder实例的最新下载请求URL(因为RecyclerView中的ViewHolder是会不断回收重用的)
最后,初始化mRequestHandler并定义该Handler在得到消息队列中的下载消息后应执行的任务:
public class ThumbnailDownloader extends HandlerThread {
...
@Override
protected void onLooperPrepared() {
mRequestHandler=new Handler(){
@Override
public void handleMessage(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);
}
}
};
}
...
private void handleRequest(final T target){
try {
final String url=mRequestMap.get(target);
if(url==null){
return;
}
byte[] bitmapBytes=new FlickrFetchr().getUrlBytes(url);
final Bitmap bitmap= BitmapFactory.decodeByteArray(bitmapBytes,0,bitmapBytes.length);
Log.i(TAG,"Bitmap created");
}catch (IOException ioe){
Log.e(TAG,"Error downloading image",ioe);
}
}
}
在onLooperPrepared()方法内,我们在Handler子类中实现了Handler.handleMessage(...)方法。HandlerThread.onLooperPrepared()是在Looper首次检查队列之前调用的,所以该方法成了创建Handler实现的好地方。
在Handler.handleMessage(...)方法中,首先检查消息类型,再获取obj值(T类型下载请求),然后将其传递给handleRequest(...)方法处理。(队列中的下载消息取出并可以处理时,就会触发调用Handler.handleMessage(...)方法)。
handleRequest()方法是执行下载动作的地方。在这里,确认URL有效之后,就将它传递给FlickrFetchr新实例。最后使用BitmapFactory把getUrlBytes(...)返回的字节数字转换为位图。
目前所有的工作就是在线程上使用handler和消息——ThumbnailDownloader把消息放入自己的收件箱,接下来学习:ThumbnailDownloader如何使用Handler访问主线程。
当前,使用ThumbnailDownloader的mRequestHandler,可以从主线程安排后台线程任务:
反过来,也可以从后台线程使用与主线程关联的Handler,安排要在主线程上完成的任务:
主线程上创建的Handler会自动与它的Looper相关联。主线程上创建的这个Handler也可以传递给另一个线程。传递出去的Handler与创建它的线程Looper始终保持联系。因此,已传出的Handler负责处理的所有消息都将在主线程的消息队列中处理。
在ThumbnailDownloader.java中,添加mResponseHandler变量,以存放来自于主线程的Handler。然后,以一个接受Handler的构造方法替换原有构造方法,并设置变量的值,最后新增一个用来(请求者和结果间)通信的监听接口,代码如下:
public class ThumbnailDownloader extends HandlerThread {
private static final String TAG="ThubnailDownloader";
private static final int MESSAGE_DOWNLOAD=0;
private Boolean mHasQuit=false;
private Handler mRequestHandler;
private ConcurrentMap mRequestMap=new ConcurrentHashMap<>();
private Handler mResponseHandler;
private ThumbnailDownloadListener mThumbnailDownloadListener;
public interface ThumbnailDownloadListener{
void onThumbnailDownloaded(T target,Bitmap thumbnail);
}
public void setThumbnailDownloadListener(ThumbnailDownloadListener listener){
mThumbnailDownloadListener=listener;
}
public ThumbnailDownloader(Handler responseHandler){
super(TAG);
mResponseHandler=responseHandler;
}
...
}
在图片下载完成,可以交给UI去显示时,定义在ThumbnailDownloadListener新接口中的onThumbnailDownloaded(...)方法就会被调用。为了把下载任务和UI更新任务分开,我们会使用这个监听器方法把处理已下载图片的任务代理给另一个类(这里指PhotoGalleryFragment),这样ThumbnailDownloader就可以把下载结果传给其他视图对象。
修改PhotoGalleryFragment类,将关联主线程的Handler传递给ThumbnailDownloader,另外再设置一个ThumbnailDownloadListener处理已下载图片:
public class PhotoGalleryFragment extends Fragment{
...
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
new FetchItemsTask().execute();
Handler responseHandler=new Handler();
mThumbnailDownloader=new ThumbnailDownloader<>(responseHandler);
mThumbnailDownloader.setThumbnailDownloadListener(new ThumbnailDownloader.ThumbnailDownloadListener() {
@Override
public void onThumbnailDownloaded(PhotoHolder target, Bitmap thumbnail) {
Drawable drawable=new BitmapDrawable(getResources(),thumbnail);
target.bindDrawable(drawable);
}
});
mThumbnailDownloader.start();
mThumbnailDownloader.getLooper();
Log.i(TAG,"Background thread started");
}
...
}
这个Handler是在onCreate(...)中创建的,因此它会与主线程的Looper相关联。
通过onThumbnailDownloaded实现,使用新下载的Bitmap来设置PhotoHolder的Drawable。
我们也可以定制Message给主线程,要求显示已下载图片,不过需要另一个Handler子类,以及一个handleMessage(...)覆盖方法。这里我们转而使用另一个方便的Handler方法——post(Runnable),Handler.post(Runnable)是一个发布Message的便利方法,具体如下:
Message没有回调方法时,它从消息队列取出后,是不会发给target Handler的,相反,存储在回调方法中的Runnable的run()方法会直接执行。
在ThumbnailDownloader.handleRequest()方法中,添加代码如下:
private void handleRequest(final T target){
try {
...
mResponseHandler.post(new Runnable() {
@Override
public void run() {
if(mRequestMap.get(target)!=url||mHasQuit){
return;
}
mRequestMap.remove(target);
mThumbnailDownloadListener.onThumbnailDownloaded(target,bitmap);
}
});
}catch (IOException ioe){
Log.e(TAG,"Error downloading image",ioe);
}
}
因为mResponseHandler与主线程的Looper相关联,所以UI更新代码会在主线程中完成。
新增下列方法清除队列中的所有请求:
public class ThumbnailDownloader extends HandlerThread {
...
public void clearQueue(){
mRequestHandler.removeMessages(MESSAGE_DOWNLOAD);
}
...
}
视图已销毁,就要在PhotoGalleryFragment.java中清空下载器:
public class PhotoGalleryFragment extends Fragment{
...
@Override
public void onDestroyView() {
super.onDestroyView();
mThumbnailDownloader.clearQueue();
}
...
}
这里不使用AsyncTask原因:
AsyncTask主要应用于短暂且较少重复的任务;从Android3.2版本起,AsyncTask不再为每一个AsyncTask实例单独创建线程,相反,它使用一个Executor在单一的后台线程上运行所有的AsyncTask后台任务。所以长时间运行的AsyncTask会阻塞其他AsyncTask。
应用加强:增加缓存层和预加载图片
Android支持库中的LruCache类实现了LRU(最近最少使用)缓存策略。
预加载是指在实际使用对象前,就预先将处理对象加载到缓存中。这样,在显示bitmap时就不会存在下载延迟。
图片下载问题:可以引用第三方库例如Picasso库