在前一篇文章中我们讨论了,BitmapFactory.decode*的方法不应该在 数据源是从磁盘或者网络位置(或者可以说不是内存的位置)的情况下在主线程中执行。加载这个数据的时间会不可预料,并且依赖于很多不同的因素(磁盘或者网络的读取速度,图片的大小,CPU的功率,等等)。如果其中的任何一个因素导致了主线程阻塞,系统就会将你的程序标记为无响应,并且用户会得到一个强制关闭的选择(在Designing for Responsiveness中可以看到更多相关信息)。
这篇教程会教你如何在后台线程中用AsyncTask处理bitmap,并且教你如何处理并发的问题。
使用AsyncTask
AsyncTask提供了一个简单的方法去在后台线程中执行任务并将结果返回到UI线程中。要使用它,首先创建它的一个子类并覆盖其提供的方法。这里有个例子,讲的是用AsyncTask和decodeSampledBitmapFromResource()(这个方法在教程1中)加载大图片到ImageView中:
class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView){
//使用WeakReference可以确保这个ImageView可以被垃圾收集器收集
imageViewReference = new WeakReference<ImageView>(imageView);
}
//一旦完成了,看看ImageView是否还在然后设置bitmap
@override
protected void onPostExecute(Bitmap bitmap){
if(imageViewReference != null && bitmap!=null){
imageView.setImageBitmap(bitmap);
}
}
}
这个ImageView 的弱引用保证了AsyncTask不阻止ImageView的回收,它引用的任何东西都可以被垃圾回收。当任务完成时,并不能保证ImageView仍然存在,所以你必须在onPostExecute()中检查。这个ImageView可能不存在了,如果是这样,那么用户就是离开这个activity了,或者是一个配置在任务完成前被改变了。
开始异步加载bitmap,只要简单的创建一个任务并执行它:
public void loadBitmap(int resId,ImageView imageView){
BitmapWorkerTask task = new BimapWorkerTask(imageView);
task.execute(resId);
}
处理并发
常见的试图组件比如ListView和GridView会导致另外一个问题。问题在于连接AsyncTask和原来展示的部分。为了提高内存的效率,这些组件会在用户滑动时回收子view。如果每个子view都触发了一个AsyncTask,那么就不能保证当其完成时,关联的view很可能会别其它的子view回收了。并且,异步任务的开始和完成的顺序也不能保证。
博客 Multithreading for Performance 更深入地讨论了处理并发的情况,并且提供了一个解决方法:ImageView保存最近的一个AsyncTask的引用,在任务完成之后可以检查一下。
使用一个类似的方法,前面部分的AsyncTask可以用类似的模式进行拓展。
创建一个专用的Drawable子类去存储工作task的引用。在这个例子中,一个BitmapDrawble被用到了,它保证了一个占位图片可以在任务完成后显示到ImageView中。
static class AsyncDrawable extends BitmapDrawable{
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AyncDrawable(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 BimmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(),mPlaceHolderBitmap,task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
引用的cancelPotentialWork方法,是用来检查是否另外一个在运行的线程早已关联了这个ImageView,如果是这样,它会通过调用cancle()的方法去试图取消原来的任务。在少数例子中,新task的数据和已存在的task数据相匹配,那么就没什么需要做的了。下面是cancelPotentialWork的实现:
public static boolean cancelPotentialWork(int data,ImageView imageView){
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if(bitmapWorkerTask != null){
//取消前一个任务
bitmapWorkerTask.cancel(true);
}else{
//同样的任务已在执行
return false;
}
//没有任务和ImageView关联,或者存在的任务已经被取消了
return true;
}
一个辅助的方法,getBitmapWorerTask(),是用来检索关联到一个特定ImageView的task。
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()方法,这样就可以检查这个任务是否被取消了,当前task是不是和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组件中稳定地使用了,简单地调用loadBitmap去设置一个图片到ImageView中。比如,在一个GridView的实例中,这个方法应该在支持它的adpater的getView()方法中。