前面我们总结了Androd中如何去加载尺寸比较大的图片。现在我们需要考虑的问题是当我们从磁盘或者网络中加载图片时,由于磁盘的读取速度或者网速的原因导致话费很长的时间去加载。如果吧这些耗时的代码放在ui线程,会导致ANR异常。 所以。这篇文章中,我们将讨论使用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)— 图片的缓存
