Android

Java安卓学习总结(三十)

第二十六章 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。

image 33

一个Handler仅与一个Looper相关联,而多个Handler可以与一个Looper关联。一个message也仅与一个Handler关联,但是一个Handler可以与多条message关联。每一个HandlerThread都有一个属于自己的Looper,每一个Handler会与当前线程的Looper关联。

image 34

创建并启动后台线程

写一个继承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中看日志信息,我将消息写在屏幕上了,可以仔细观察左上角,发现确实有效果:

ScreenRecord 2020 07 04 21 02 261

预加载

预加载比缓存复杂多了,首先要监听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());
                }

            }
        }
    }
...
}

往下翻动的时候可以看见隔了一行的已经被下载好了,可惜由于是魔法上网,导致网速跟不上,所以只能加载一点点。不过效果算是达到了。

ScreenRecord 2020 07 04 21 50 071

发表评论