Processing Bitmaps Off the UI Thread 非UI线程加载图片
BitmapFactory.decode*一系列方法,在之前的高效加载大图的文章中讲到过。
如果图片的数据源是磁盘,或则网络(内存以外的其他地方),那么解析图片的方法不应该在UI线程中执行。这些数据加载任务所要花费的时间有许多不可控因素,(例如:磁盘读取速度,图片的大小,CPU的频率,等等)如果这些任务阻塞了UI线程,系统判定你的应用程序无响应,用户是有权关闭你的软件的,这样的用户体验非常不好。
这篇文章主要用来讲解如何使用AsyncTask在后台线程处理图片加载过程,以及并发问题的处理。
Use an AsyncTask 使用AsyncTask
AsyncTask这个类使得 在后台处理某些任务变得更加简单,处理完任务之后再将结果 返回给UI线程。使用它的方式就是自己写一个子类覆盖AsyncTask中的方法。下面是一个通过AsyncTask和decodeSampledBitmapFromResource()方法来加载图片进入ImageView的一个例子:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
这个ImageView 的弱引用(WeakReference)保证了AsyncTask对象不会阻止 这个ImageView和引用了ImageView的对象被垃圾回收机制回收。 当AsyncTask任务执行完了之后,不能保证ImageView任然存在,所以你必须 在onPostExecute()方法中检查 一下这个引用。 这个ImageView可能不会长久存在,比如当用户退出当前Activity,或则任务完成之后 配置环境发生变化。
实现异步的加载图片的任务只需要新建一个Task然后execute一下。
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
Handle Concurrency 处理并发问题
一些常用的View组件,ListView和GridView与AsyncTask结合起来展示当前章节的内容的时候,会引发一系列问题。 为了高效的使用内存,这些控件会在用户滑动屏幕的时候回收子控件view对象。 如果每一个子类的view对象都触发一个AsyncTask,这就不能保证当AsyncTask任务执行完时,相关的view对象还没有被回收,从而用来展示其他的view。
而且并不能保证异步任务的开始顺序和结束顺序的一致性。
这篇博客 讲了 Multithreading for Performance 来解决并发问题的讨论,而且提供了一个解决办法.
创建一个Drawable的子类去存储一个指向worker task的引用.在这种情况下,BitmapDrawable就可拿出来使用了。这样的话 当后台任务执行完之后,image的容器 PlaceHoler 就可以将image显示在ImageView中了。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
在执行BitmapWorkerTask之前,你需要创建一个AsyncDrawable 然后将它绑定到目标的ImageView上面。
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
这个cancelPotentialWork方法 可以检查上面的示例代码中的imageView是否有多个正在运行的任务。(其实就是验证一个imageView是否只有一个后台任务运行,避免一个imageView绑定多个后台任务,造成资源的浪费)
如果 是,则通过 cancel()方法来取消当前的任务。 其实在少数情况下,即使发现了新开的后台任务跟之前的有重复,但是也不会取消掉这个新开的任务。 下面是这个方法的实现。
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// If bitmapData is not yet set or it differs from the new data
//
if (bitmapData == 0 || bitmapData != data) {
//取消当前任务
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
//当前ImageView没有其他的任务与它关联,或则有其他的任务与他关联但是已经取消掉了
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
还有一个帮助方法,getBitmapWorkerTask 用来获得与这个ImageView想关联的任务
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
最后一步就是更新(上传)通过BitmapWorkerTask 中的onPostExecute()方法来检查 当前任务是否被取消 ,是否与 这个ImageView相关联。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
这个实现类现在可以 简单的用在ListView和GridView中了。 一些其他的需要回收子类View的组件,也可以使用这个类。
在你需要为 ImageView设置 一个image的时候简单的调用一下 loadBitmap这个方法就可以实现复杂的后台加载任务了。
比如说:在GridView中实现这个过程就可以后台的Adapter中的getView()方法中修改。
这篇 示例的完整实现类,请看郭霖的博客文章。