前面我们总结了Androd中如何去加载尺寸比较大的图片。现在我们需要考虑的问题是当我们从磁盘或者网络中加载图片时,由于磁盘的读取速度或者网速的原因导致话费很长的时间去加载。如果吧这些耗时的代码放在ui线程,会导致ANR异常。
所以。这篇文章中,我们将讨论使用AsyncTask在后台线程中去加载图片,并且最后将会教你如果处理并发问题。
使用AsyncTask
AsyncTask类提供了一些的方法在后台线程执行一些耗时操作,并且把最终的执行结果发布到ui线程。
AsyncTask使用步骤,首先,创建一个类去继承AsyncTask。然后重写它的一些方法,下面这断代码使用了AsyncTask和decodeSampledBitmapFromResource()(注:此方法为以合适的缩放比加载bitmap图片,具体请看上一节Android中Bitmaps 处理详解(1)中此方法的创建过程)把一张尺寸较大的图片加载进ImageView。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// 使用软引用是为了确保ImageView可以被及时的回收
imageViewReference = new WeakReference<ImageView>(imageView);
}
// 在后台线程中获取图片并转为bitmap.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// 获取到bitmap后,判断如果ImageView没有被回收,则吧图片加载进ImageView
@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持有的其他引用被及时回收。所以在后台执行完任务后,我们不确定ImageView是否被回收,所以在**onPostExecute()**方法中判断ImageView是否为null, 比如,如果用户在AsyncTask执行结束前关闭了Activity界面。
通过以上封装,我们在后台线程中加载图片可以简答的用下面的代码就可完成:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
处理并发问题
常见的视图组件,如ListView和GridView,在使用前面AsyncTask操作时面对同样的问题就是,为了消耗内存,这些组件在滚动的时候会去循环利用他们的子View,如果每个子View都执行了AsyncTask,所以我们不能确定,某个item执行了AsyncTask,当Async执行完毕后,该item是否还存在,同样,我们也不能保证AsyncTask的启动顺序就是AsyncTask的完成顺序。
有国外大牛给出了解决方法,用ImageView存储最近的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();
}
}
我们看到上面代码中,先不管它的父类BitmapDrawable和弱引用WeakReference;我们简化上面的代码就是:
static class AsyncDrawable {
BitmapWorkerTask bitmapWorkerTaskReference;
public AsyncDrawable( BitmapWorkerTask bitmapWorkerTask) {
bitmapWorkerTaskReference = bitmapWorkerTask;
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference;
}
}
通过简化的代码,我们知道,上步我们只是创建了一个类,然后在类中创建一个成员变量BitmapWorkerTask,然后添加set/get方法来设置获取BitmapWorkerTask。
回到我们的问题,我们前面说过,要把任务与ImageView绑定到一起,即把
BitmapWorkerTask和ImageView绑定到一起,测试我们想到的ImageView有setTag()和getTag()方法,可以完成,当然,这也是一种思路。
现在国外大牛提供了一种更巧妙的解决方法就是把通过ImageView的setImageDrawable()方法和getDrawable()方法来进行绑定,这样绑定的好处因为他的参数是Drawable对象,我们可以通过在Drawable的子类当中来设置任务,然后当任务执行前,ImageView就去加载Drawable子类所对应的图片,任务执行结束后,ImageView已经加载了需要去加载的图片。这样的Drawable就相当于一个placeholder占位图 ,实现了ImageView对任务的绑定。
所以我们上面类 需要去继承BitmapDrawable类。而用软引用的作用前面已经说过了,这里再不做重复说明了。
这会儿我们看看这个并发问题中处理加载图片的最终代码:
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
// 创建AsyncDrawable对象
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
// 绑定ImageView与任务
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
我们看到,在执行任务前,我们首先调用了cancelPotentialWork(resId, imageView)方法,那么这个方法是干什么用的呢?
下面为cancelPotentialWork(resId, imageView)方法的代码:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
// 获取当前ImageView绑定的任务
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
// 获取到任务执行的资源Id
final int bitmapData = bitmapWorkerTask.data;
// 判断id是否被设置过,跟当前的id是否相同
if (bitmapData == 0 || bitmapData != data) {
// 如果不相同,退出任务
bitmapWorkerTask.cancel(true);
} else {
// 如果相同,则说明当前的任务正在运行。
return false;
}
}
// 当前的ImageView没有绑定任务,或者任务已经运行完成
return true;
}
我们看到上面代码中有个方法,getBitmapWorkerTask(imageView),这个方法就是获取传入的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;
}
下面来解释一下代码:
首先,我们会调用loadBitmap(int resId, ImageView imageView)传入要加载的图片的资源ID,和图片显示的控件去加载图片, 先去获取控件ImageView所绑定的加载任务,判断当前ImageView所绑定的任务(如果绑定了任务)中加载的图片资源Id是否与当前即将要被加载的图片资源ID相同,如果不同,则退出当前ImageView所绑定的任务,重新开始新的任务去加载当前传入的资源id。
如果当前的资源id与ImageView所绑定的任务正在加载的资源ID相同,则让它继续执行任务,不去干预。
最后我们重新去修改 BitmapWorkerTask中的**onPostExecute()**里面的代码如下:
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);
}
}
}
}
修改的代码主要作用是检查当前的任务是否被取消,和当前任务是否是ImageView控件所绑定的任务。
经过以上的封装,我们就可以在诸如ListView、GridView和其他任何复用子View的控件去加载图片。
请继续阅读下篇Android如何高效的加载图片(3)— 图片的缓存