17 Looper、Handler和HandlerThread

配置RecyclerView以显示图片

新建布局文件

ImageView 由 RecyclerView 的 GridLayoutManager 负责管理,这意味着其宽度会变,而高度保持固定不变 。为最大化利用ImageView的空间,应设置它的 scaleType 属性值为centerCrop 。这个属性值的作用是先居中放置图片,然后放大较小图片,裁剪较大图片(裁两头)以匹配视图。

gallery_item.xml

<ImageView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_photo_gallery_image_view"
    android:layout_width="match_parent"
    android:layout_height="120dp"
    android:layout_gravity="center"
    android:scaleType="centerCrop">
</ImageView>

然后,需要更新PhotoHolder,需要初始化ImageView并且修改绑定数据的方法;接着更新Adapter,初始化新建的布局文件。

绑定默认图片

为每个 ImageView 设置占位图,等成功下载图片后再对其进行替换。首先,复制图片文件到res/drawable目录,然后为其设置占位图。

PhotoGalleryFragment.PhotoAdapter

@Override
public void onBindViewHolder(@NonNull PhotoHolder holder, int position) {
    GalleryItem galleryItem = mGalleryItemList.get(position);
    Drawable placeholder = getResources().getDrawable(R.drawable.loading_image);
    holder.bindDrawable(placeholder);

    //关联HandlerThread后台线程
    mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
}

批量下载缩略图

如果在FetchItemTask的doInBackground方法中进行网络图片下载,再执行onPostExecute方法在视图上显示图片,会存在一些问题。不仅下载耗时,而且UI无法在下载完成前更新,等等其它问题。

考虑到这类问题,很多现实应用通常会选择仅在需要显示图片时才去下载。显然,RecyclerView 及其 adapter 应负责实现按需下载 。 adapter 触发图片下载就放在onBindViewHolder(…) 方法中实现。


与主线程通信

消息队列

Android系统中,线程使用的收件箱叫作消息队列(message queue)

消息循环

使用消息队列的线程叫作消息循环(message loop)

  • 消息循环会检查队列上是否有新消息。
  • 消息循环由线程和looper组成。
  • Looper对象管理着线程的消息队列。

线程就是个消息循环,因此也拥有looper。主线程的所有工作都是由其looper完成的。 looper不断从消息队列中抓取消息,然后完成消息指定的任务。


创建并启动后台线程

初始化线程

创建一个同样是消息循环的后台线程。

ThumbnailDownloader.java

public class ThumbnailDownloader<T> extends HandlerThread {
    private static final String TAG = "ThumbnailDownloader";
    
    //ThumbnailDownloader是否已经退出
    private Boolean mHasQuit = false;

    public ThumbnailDownloader(Handler responseHandler) {
        super(TAG);
    }

    @Override
    public boolean quit() {
        mHasQuit = true;
        return super.quit();
    }

    public void queueThumbnail(T target, String url) {
        Log.i(TAG, "得到URL:" + url);
    }
}

queueThumbnail() 方法需要一个 T 类型对象(标识具体那次下载)和一个 String 参数(URL下载链接)。同时,它也是 PhotoAdapter 在其 onBindViewHolder(…) 实现方法中要调用的方法。

创建ThumbnailDownloader

为 PhotoGalleryFragment 添加一个 ThumbnailDownloader 类型的成员变量。然后,在 onCreate(…) 方法中,创建并启动线程。最后,覆盖onDestroy() 方法退出线程,如代码清单24-5所示。

PhotoGalleryFragment.java

private ThumbnailDownloader<PhotoHolder> mThumbnailDownloader;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    new FetchItemsTask().execute();

    //将关联主线程的Handler传递给ThumbnailDownloader
    Handler responseHandler = new Handler();
    mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler);

    //设置一个ThumbnailDownloaderListener处理已下载图片
    //该监听器使用返回的Bitmap执行UI更新操作
    //...

    mThumbnailDownloader.start();
    mThumbnailDownloader.getLooper();
    Log.i(TAG, "后台线程启动...");
}

@Override
public void onDestroy() {
    super.onDestroy();

    mThumbnailDownloader.quit();
    Log.i(TAG, "后台线程销毁...");
}

需要注意的有:

  • 在 ThumbnailDownloader 线程上,在 start() 方法之后调用 getLooper() 方法。
    这是一种保证线程就绪的处理方式,可以避免潜在竞争(尽管极少发生)。调用 getLooper() 方法之前,没办法保证 onLooperPrepared()方法已得到调用。所以, Handler 为空的话,调用 queueThumbnail() 方法很可能会失败。

  • 在 onDestroy() 方法内调用 quit() 方法结束线程。这非常关键。如不终止 HandlerThread,它会一直运行下去。

关联使用ThumbnailDownloader

在 PhotoAdapter.onBindViewHolder(…) 方法中,调用线程的 queueThumbnail()方法,并传入放置图片的 PhotoHolder 和 GalleryItem 的URL。

PhotoGalleryFragment.PhotoAdapter

@Override
public void onBindViewHolder(@NonNull PhotoHolder holder, int position) {
    GalleryItem galleryItem = mGalleryItemList.get(position);
    Drawable placeholder = getResources().getDrawable(R.drawable.loading_image);
    holder.bindDrawable(placeholder);

    //关联HandlerThread后台线程
    mThumbnailDownloader.queueThumbnail(holder, galleryItem.getUrl());
}

成功创建并运行 HandlerThread 线程后,接下来的任务是:使用传入 queueThumbnail(…) 方法的信息创建消息,并放置在 ThubnailDownloader 的消息队列中。


message与message handler

message

消息是Message类的一个实例,它有好几个实例变量,其中有三个需在实现时定义:

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

Message的target是 Handler 类的一个实例。 Handler 可看作message handler的简称。创建Message 时,它会自动与一个 Handler 相关联。 Message 待处理时,Handler 对象负责触发消息处理事件。

handler

要处理消息以及消息指定的任务,首先需要一个 Handler 实例。 Handler 不仅仅是处理Message 的目标(target),也是创建和发布 Message 的接口。

在这里插入图片描述

Looper 拥有 Message 收件箱,所以 Message 必须在 Looper 上发布或处理。既然有这层关系,为与 Looper 协同工作,Handler总是引用着它。

一个 Handler 仅与一个 Looper 相关联,一个 Message 也仅与一个目标 Handler (也称作Message 目标)相关联。 Looper 拥有整个 Message 队列。多个 Message 可以引用同一目标 Handler 。

多个 Handler 也可与一个 Looper 相关联。这意味着一个 Handler 的 Message 可能与另一个Handler 的 Message 存放在同一消息队列中。

通常不需要手动设置消息的目标 Handler 。创建信息时,最好调用 Handler.obtainMessage(…) 方法。当传入其他消息字段给它时,该方法会自动设置目标给 Handler 对象。为避免创建新的 Message 对象,Handler.obtainMessage(…) 方法会从公共循环池里获取消息。相比创建新实例,这样显然更有效率。

一旦取得 Message,就可以调用 sendToTarget() 方法将其发送给它的 Handler 。然后,Handler 会将这个 Message 放置在 Looper 消息队列的尾部。

Looper 取得消息队列中的特定消息后,会将它发送给消息目标去处理。消息一般是在目标的Handler.handleMessage(…) 实现方法中进行处理的。


使用handler

获取和发送消息

ThumbnailDownloader.java

//标识下载请求信息
private static final int MESSAGE_DOWNLOAD = 0;

//存储对Handler的引用,此Handler负责在ThumbnailDownloader后台线程上管理下载请求消息队列
private Handler mRequestHandler;

//线程安全的HashMap,用来存取下载请求和与请求关联的URL下载链接
private ConcurrentMap<T, String> mRequestMap = new ConcurrentHashMap<>();

public void queueThumbnail(T target, String url) {
    Log.i(TAG, "得到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值(这里指 PhotoHolder )。

新消息就代表指定为 T target ( RecyclerView 中的 PhotoHolder )的下载请求。PhotoGalleryFragment 中,RecyclerView 的adapter实现就是从 onBindViewHolder(…) 方法里调用 queueThumbnail(…) ,把待下载图片及其URL传给 PhotoHolder 。

注意,消息自身是不带URL信息的。我们的做法是使用 PhotoHolder 和URL的对应关系更新mRequestMap 。 随后,我们会从 mRequestMap 中取出图片URL,以保证总是使用了匹配PhotoHolder 实例的最新下载请求URL。(这很重要,因为 RecyclerView 中的 ViewHolder 是会不断回收重用的。)

处理消息

最后,初始化 mRequestHandler 并定义该 Handler 在得到消息队列中的下载消息后应执行的任务。

ThumbnailDownloader.java

@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, "收到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 FlickrFetcher().getUrlBytes(url);
        final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
        Log.i(TAG, "Bitmap已创建。");
    }
    catch (IOException ioe) {
        Log.e(TAG, "下载图片发生错误!", ioe);
    }
}

在 onLooperPrepared() 方法内,我们在 Handler 子类中实现了 Handler.handleMessage(…)方法。 HandlerThread.onLooperPrepared() 是在 Looper 首次检查消息队列之前调用的,所以该方法成了创建 Handler 实现的好地方。


传递handler

处理消息

当前,使用 ThumbnailDownloader 的 mRequestHandler,可以从主线程安排后台线程任务,反过来,也可以从后台线程使用与主线程关联的Handler,安排要在主线程上完成的任务。

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

添加上述 mResponseHandler 变量,以存放来自于主线程的Handler 。然后,以一个接受 Handler 的构造方法替换原有构造方法,并设置变量的值,最后新增一个用来(在请求者和结果间)通信的监听器接口。

ThumbnailDownloader.java

//存放来自于主线程的Handler
private Handler mResponseHandler;

//用来在请求者和结果间进行通信的监听器接口
private ThumbnailDownloaderListener<T> mThumbnailDownloaderListener;

public interface ThumbnailDownloaderListener<T> {
    void onThumbnailDownloaded(T target, Bitmap thumbnail);
}

public void setThumbnailDownloaderListener(ThumbnailDownloaderListener<T> listener) {
    mThumbnailDownloaderListener = listener;
}

public ThumbnailDownloader(Handler responseHandler) {
    super(TAG);
    mResponseHandler = responseHandler;
}

在图片下载完成,可以交给UI去显示时,定义在 ThumbnailDownloadListener 新接口中的onThumbnailDownloaded(…) 方法就会被调用。稍后,为了把下载任务和UI更新任务(把图片放入 ImageView )分开,我们会使用这个监听器方法把处理已下载图片的任务代理给另一个类(这里指 PhotoGalleryFragment),而不是 ThumbnailDownloader 。这样,ThumbnailDownloader就可以把下载结果传给其他视图对象。

关联使用反馈handler

接下来,修改 PhotoGalleryFragment 类,将关联主线程的Handler传递给 ThumbnailDownloader 。另外,再设置一个 ThumbnailDownloadListener 处理已下载图片。

PhotoGalleryFragment.java

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    new FetchItemsTask().execute();

    //将关联主线程的Handler传递给ThumbnailDownloader
    Handler responseHandler = new Handler();
    mThumbnailDownloader = new ThumbnailDownloader<>(responseHandler);

    //设置一个ThumbnailDownloaderListener处理已下载图片
    //该监听器使用返回的Bitmap执行UI更新操作
    mThumbnailDownloader.setThumbnailDownloaderListener(
            new ThumbnailDownloader.ThumbnailDownloaderListener<PhotoHolder>() {
                @Override
                public void onThumbnailDownloaded(PhotoHolder photoHolder, Bitmap thumbnail) {
                    Drawable drawable = new BitmapDrawable(getResources(), thumbnail);
                    photoHolder.bindDrawable(drawable);
                }
            }
    );

    mThumbnailDownloader.start();
    mThumbnailDownloader.getLooper();
    Log.i(TAG, "后台线程启动...");
}

图片下载与显示

和在后台线程上把下载图片的请求放入消息队列类似,我们也可以返回定制 Message 给主线程,要求显示已下载图片。不过,这需要另一个 Handler 子类,以及一个 handleMessage(…)覆盖方法。方便起见,我们转而使用另一个方便的 Handler 方法 post(Runnable) 。

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

ThumbnailDownloader.java

private void handleRequest(final T target) {
    try {
        final String url = mRequestMap.get(target);

        if (url == null) return;

        byte[] bitmapBytes = new FlickrFetcher().getUrlBytes(url);
        final Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapBytes, 0, bitmapBytes.length);
        Log.i(TAG, "Bitmap已创建。");

        //返回定制Message给主线程,要求显示已下载图片
        //Message设有回调方法时,它从消息队列取出后,是不会发给target Handler的,
        //而存储在回调方法中的Runnable的run()方法会直接执行
        mResponseHandler.post(new Runnable() {
            @Override
            public void run() {
                //保证每个PhotoHolder都能获取到正确的图片,即使中间发生了其他请求也无妨
                //如果ThumbnailDownloader已经退出,运行任何回调方法可能都不太安全
                if (mRequestMap.get(target) != url || mHasQuit) return;

                //从requestMap中删除配对的PhotoHolder URL
                mRequestMap.remove(target);

                //将位图设置到目标PhotoHolder上
                mThumbnailDownloaderListener.onThumbnailDownloaded(target, bitmap);
            }
        });
    }
    catch (IOException ioe) {
        Log.e(TAG, "下载图片发生错误!", ioe);
    }
}

清理消息队列

新增方法清除队列中的所有请求:

ThumbnailDownloader.java

//清除队列中的所有请求
public void clearQueue() {
    mRequestHandler.removeMessages(MESSAGE_DOWNLOAD);
}

然后在PhotoGalleryFragment.onDestroyView()方法中调用该方法清空下载器:

PhotoGalleryFragment.java

@Override
public void onDestroyView() {
    super.onDestroyView();
    mThumbnailDownloader.clearQueue();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值