第二十六章 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());
}
}
}
}
...
}
往下翻动的时候可以看见隔了一行的已经被下载好了,可惜由于是魔法上网,导致网速跟不上,所以只能加载一点点。不过效果算是达到了。
