第二十六章 Looper、Handler和HandlerThread
有关HandlerThread工作机制
AsyncTask是一个简单的后台线程执行方式,由于自Android3.2开始,所有AsyncTask后台线程都在同一个单一的后台线程上线性执行,所以其不适合复杂且长时间运行的任务,只能执行短暂且重复较少的任务。然后下载图片耗时很长,且当屏幕翻动时会伴随着每一张图片的重复下载,因此需要使用HandlerThread来为下载的每一张图片建立一个专用的线程。
HandlerThread还与Looper和Handler有关。Looper和线程一起组成了安卓系统的消息循环,且其管理者线程的消息队列;消息队列中的每一个消息(message)十分显然地需要在Looper上发布或者处理,一个message实例不需要我们手动去创建,只不过其中有三个变量需要我们定义:what(消息代码)、obj(随消息发送的对象)、target(处理消息的Hander);除处理消息之外,Handler就是创建和发布message的接口,此外由于每一个message都在Looper中发布和处理,所以Handler总是引用着Looper。
一个Handler仅与一个Looper相关联,而多个Handler可以与一个Looper关联。一个message也仅与一个Handler关联,但是一个Handler可以与多条message关联。每一个HandlerThread都有一个属于自己的Looper,每一个Handler会与当前线程的Looper关联。
创建并启动后台线程
写一个继承HanlderThread的类,在fragment中启动它:
public class PhotoGalleryFragment extends Fragment { ... private ThumbnailDownloader<PhotoHolder> mThumbnailDownloader; ... @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); ... mThumbnailDownloader=new ThumbnailDownloader<>(responseHandler); ... mThumbnailDownloader.start(); mThumbnailDownloader.getLooper(); Log.i(TAG,"background thread started"); }
获取、发送和处理消息
消息的obj是从fragment的adapter中调用方法传入参数过来的,发送消息是利用Handler,此外还需要先定义消息代码常量:
public void queueThumbnail(T target,String url) { Log.i(TAG,"Got a URL:" + url); PhotoGalleryFragment.setText("Got a URL:"+ url); if (url==null) mRequestMap.remove(target); else { mRequestMap.put(target,url); mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD,target).sendToTarget(); } }
处理消息是在实例化Handler时重载其handleMessage方法中实现的:
@Override protected void onLooperPrepared() { mRequestHandler=new Handler() { @Override public void handleMessage(@NonNull Message msg) { if (msg.what==MESSAGE_DOWNLOAD) { T target=(T)msg.obj; handleRequest(target); } } }; } private void handleRequest(final T target) { try { final String url=mRequestMap.get(target); if ((url==null)) { return; } byte[] bitmapByte=new FlickrFetchr().getUrlBytes(url); final Bitmap bitmap= BitmapFactory.decodeByteArray(bitmapByte,0,bitmapByte.length); ... } catch (IOException ioe) { } }
HandlerThread更新UI
AsyncTask是利用onPostExecyte方法来更新UI的,这个方法有两个特征:一是保证在自己的线程的任务全都完成之后再调用此方法,而是此方法会在主线程中执行。这两个特征可以说是更新UI的两个必要条件。
只有在HandlerThread中调用更新UI的方法才能确定子线程何时处理结束,要满足第二个条件,可以在HandlerThread引用主线程的Handler,并用主线程的Handler的post(Runnable)来实现UI的更新操作:
public void queueThumbnail(T target,String url)
{
Log.i(TAG,"Got a URL:" + url);
PhotoGalleryFragment.setText("Got a URL:"+ url);
if (url==null) mRequestMap.remove(target);
else
{
mRequestMap.put(target,url);
mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD,target).sendToTarget();
}
}
public void clearQueue()
{
mRequestHandler.removeMessages(MESSAGE_DOWNLOAD);
mRequestMap.clear();
}
private void handleRequest(final T target)
{
try
{
final String url=mRequestMap.get(target);
if ((url==null))
{
return;
}
byte[] bitmapByte=new FlickrFetchr().getUrlBytes(url);
final Bitmap bitmap= BitmapFactory.decodeByteArray(bitmapByte,0,bitmapByte.length);
mResponseHandler.post(new Runnable() {
@Override
public void run() {
if (mRequestMap.get(target)!=url||mHasQuit)
{
return;
}
mRequestMap.remove(target);
mThumbnailDownloaderListener.onThumbnailDownloaded(target,bitmap);
}
});
}
catch (IOException ioe)
{
}
}
有关挑战练习 预加载以及缓存
缓存
缓存是比较简单的,直接套一个模板就行了。模板
两个新类是直接拿过来抄的。主要记录在我这个应用中如何实现。
在Adapter的onBindViewHolder方法中先进行判断,如果在缓存中就直接用缓存的了,如果不在缓存中就要下载:
@Override public void onBindViewHolder(@NonNull PhotoHolder holder, int position) { GalleryItem galleryItem=mGalleryItems.get(position); Drawable placeholder=getResources().getDrawable(R.drawable.bill_up_close); holder.bindGalleryItem(placeholder); Bitmap bitmap = getBitmapFromCache(galleryItem.getUrl()); if (bitmap != null) {//有缓存 Drawable drawable=new BitmapDrawable(getResources(),bitmap); holder.bindGalleryItem(drawable); } else {//没有缓存 mThumbnailDownloader.queueThumbnail(holder,galleryItem.getUrl()); } }
下载完之后还需要将下好的照片存到缓存中去,所以要修改回调的接口方法:
mThumbnailDownloader.setThumbnailDownloaderListener(new ThumbnailDownloader.ThumbnailDownloaderListener<PhotoHolder>() { @Override public void onThumbnailDownloaded(PhotoHolder target, Bitmap thumbnail,String url) { mMyImageLoader.addBitmap(ImageUtils.hashKeyForCache(url), thumbnail); Drawable drawable=new BitmapDrawable(getResources(),thumbnail); target.bindGalleryItem(drawable); } });
由于不能再AVD中看日志信息,我将消息写在屏幕上了,可以仔细观察左上角,发现确实有效果:
预加载
预加载比缓存复杂多了,首先要监听RecyclerView的滑动(在上一章挑战练习中已经实现了),还要在线程中专门添加一个预下载的功能,为了存到缓存中去,还要再新写一个接口。
在线程中新增预下载的功能:
@Override protected void onLooperPrepared() { mRequestHandler=new Handler() { @Override public void handleMessage(@NonNull Message msg) { if (msg.what==MESSAGE_DOWNLOAD) { T target=(T)msg.obj; handleRequest(target); } else if (msg.what==PRE_DOWNLOAD) { String url=(String) msg.obj; handlePreDownload(url); } } }; } ... public void queuePreDownload(String url) { if (url==null) return; else { mRequestHandler.obtainMessage(PRE_DOWNLOAD,url).sendToTarget(); } } ... private void handlePreDownload(final String url) { try { if ((url==null)) { return; } byte[] bitmapByte=new FlickrFetchr().getUrlBytes(url); final Bitmap bitmap= BitmapFactory.decodeByteArray(bitmapByte,0,bitmapByte.length); mResponseHandler.post(new Runnable() { @Override public void run() { mThumbnailDownloaderListener.onPreDownloaded(bitmap,url); } }); } catch (IOException ioe) { } }
新增接口以缓存:
public interface ThumbnailDownloaderListener<T>{ void onThumbnailDownloaded(T target,Bitmap thumbnail,String url); void onPreDownloaded(Bitmap thumbnail,String url); }
mThumbnailDownloader.setThumbnailDownloaderListener(new ThumbnailDownloader.ThumbnailDownloaderListener<PhotoHolder>() {
@Override
public void onThumbnailDownloaded(PhotoHolder target, Bitmap thumbnail,String url) {
mMyImageLoader.addBitmap(ImageUtils.hashKeyForCache(url), thumbnail);
Drawable drawable=new BitmapDrawable(getResources(),thumbnail);
target.bindGalleryItem(drawable);
}
@Override
public void onPreDownloaded(Bitmap thumbnail, String url) {
mMyImageLoader.addBitmap(ImageUtils.hashKeyForCache(url), thumbnail);
}
});
在监听滑动时开启预下载:
public class RecyclerViewScrollListener extends RecyclerView.OnScrollListener implements BottomListener { // 最后一个完全可见项的位置 private int lastCompletelyVisibleItemPosition; ... @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { ... for (int i=1;i<=10;i++) { int position=lastCompletelyVisibleItemPosition+i; if (position<=totalItemCount) { GalleryItem galleryItem=mItems.get(position); Bitmap bitmap = getBitmapFromCache(galleryItem.getUrl()); if (bitmap != null) {//有缓存 } else {//没有缓存 mThumbnailDownloader.queuePreDownload(galleryItem.getUrl()); } } } } ... }
往下翻动的时候可以看见隔了一行的已经被下载好了,可惜由于是魔法上网,导致网速跟不上,所以只能加载一点点。不过效果算是达到了。