在我们上一节讨论的课程里面,BitmapFactory.decode*这个方法,是从磁盘或者网络(或者任何其他不是从内存读取的位置)读取位图资源的时候,不可以在主UI线程中进行。载入数据的花费的时间是不可预测的,这个耗时和很多因素有关系,如从磁盘或者网络读取的数据的速度,图像的大小,CPU的执行速度等等因素。如果一旦解码图像的任务在主UI线程里面阻塞了,系统就会把你的APP标示为无响应,会提示用户去关闭这个程序。想了解更多,阅读:Designing for Responsiveness
这个章节将引导力利用
AsyncTask来后台处理图片操作以及如果处理并发的问题。
AsyncTask使用
AsyncTask这个类提供一种简单的途径,可以让一些工作在后台运行,运行完成后在把后台运行的结果返回给主UI线程。使用这个类的方式是我们要实现一个这个类的子类,然后重写里面的一些方法。下面例子就是使用
AsyncTask
和decodeSampledBitmapFromResource()
方法来载入一张大的图片:
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);
}
}
}
}
WeakReference
引用到 ImageView保证了
AsyncTask不会阻碍
ImageView以及任何它的相关信息都不会被垃圾回收。没有办法保证在这个
AsyncTask结束的时候,
ImageView还是存在的,所以你必须在
onPostExecute()里面检查这一项。ImageView也许不会一直存在,例如,用户跳转到另外一个页面,或者是用户界面在这个任务结束之前发生了改变。要载入一个bitmap,只需要简单的创建这个任务,并且执行它:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
处理并发
一般的视图组件,如ListView
和 GridView当和
AsyncTask关联起来,如上面的例子中使用的时候,会产生一些其他的问题。为了高效的利用内存,这些组件的子视图在用户进行滚动操作时候,会被回收利用。如果每一个子视图都触发一个
AsyncTask,那么就无法保证,当这个执行完成的时候,这个子视图没有被系统回收作为另外一个视图的子视图来使用。此外,也无法保证异步启动任务在完成的时候的顺序也是按照他们启动的顺序完成的。
文章将进一步讨论并发的处理,并提供一种解决方法在与之关联,在
AsyncTask在任务完成的时候,对与之关联存放ImageView的位置进行检查。使用类似的方法,我们把上面的代码扩展一下,得到下面的代码段。
创建一个专用的Drawable的子类,来存放工作任务返回时候的信息。在这个例子里面, 使用了一个
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();
}
}
在执行
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,它会努力去尝试调用cancel()来取消前面的任务执行。在少量的情况下,如果新的任务和旧的任务是一样的,那么就不需要去做什么了。下面是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;
}
}
// 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
and GridView组件,以及其他的会遇到子视图被回收的组件。当你需要给你的ImageView设置一张图片的时候,那么简单的调用
loadBitmap就可以了。
比如,在一个
GridView里面,这个调用要在它的适配器的
getView()
方法里面。