在UI线程以外处理图片
——Android官网原文翻译
在"高效的加载图片"课程中我们讨论了BitmapFactory.decode方法,但是如果图片资源是从网络或者外存(或者其他非内存的存储位置)读取 的,那么我们不应该在UI线程中执行加载图片的操作。因为图片加载的时间是不可预期的并由众多的因素所决定(外存储器或者网络的读取速度,图片的大小,CPU的性能,等等)。如果加载图片的操作阻塞了UI线程,系统会把您的应用程序标识为"未响应"并提示用户关闭它(详情请参见响应式设计)。
这一节将要介绍AsyncTask的使用,并介绍怎样处理并发问题。
1.使用AsyncTask
AsyncTasks类提供了简单的方式用来在后台线程执行一些任务,并把结果传递到到UI线程。要使用这个类,就要创建一个继承了该类的子类,并重写一些方法。这里有一个使用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引用的内容不被GC回收。我们这里并不能保证当任务结束的时候ImageView的引用仍然可用,所以您也必须在onPostExecute()中检查引用。举个例子,当用户从当前Activity跳转到其他Activity或者某个设置在任务结束前发生变化的话,这个ImageView可能会不在存在。
如果要一步的加载一张图片,只需创建一个AsyncTask对象并调用execute方法。
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
2处理并发
通常,当使用像ListView或者GridView这类UI组件同时的使用上一节的AsyncTask加载图片时会遇到问题。为了有效的使用内存,这些控件会在滚动时回收子View,如果其中的每一个View都触发了一个AsyncTask,那么我们不能保证这些AsyncTask已经完成了,那么该相关联的View实际上并没有被回收。此外,我们也不能保证多个AsyncTask结束时的顺序会和它们开始时的顺序一样。
"使用多线程提升性能"这篇博客深入的讨论了并发性,并且提出了一个解决方案——ImageView保留一个最近AsyncTask的引用,该AsyncTask在任务完成时可以被检查到。使用相同的方法,上一节中的AsyncTask可以使用以下相同的模式进行扩展。
创建一个专用的Drawable子类来保存对AsyncTask的引用,既然这样,一个BitmapDrawable对象被用来当任务结束时在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();
}
}
在执行BitampWorkerTask之前,您需要创建一个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);
}
}
示例代码中使用的cancelPotentailWork方法,用来检查是否有其他正在运行的任务关联了ImageView。如果存在,它将尝试使用cacel()方法取消上一个任务。在少数情况下,新的任务数据与已经存在的任务的数据一样的话,则什么都不需要发生。以下是cancelPotentialWork方法的实现:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
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中像其他组件回收它们的子views一样使用了。当您想要让一张图片在ImageView中显示时,只需简单的调用loadBitmap即可。举个例子,如果用来在一个GridView中显示图片的话,那么这个方法可以在GridView的adapter的getView()方法中调用。