如果图片数据来源是来自于sd卡或是网络(只要不是从内存中读取),BitmapFactory类的几种解码方法的执行,就应该和UI主线程相分离。因为这些情况下加载图片数据的时间依赖于很多因素(sd卡的读写速度,网络的速度,图片的大小,cpu的性能等),因此这个时间是不确定的。如果加载图片导致UI线程的阻塞,应用会变得不可响应,这种糟糕的体验很有可能让用户直接关掉你的应用。
本节课带你过一遍如何用AsyncTask起后台进程来处理位图,并且阐释如何处理线程并发的问题。
- 使用AsyncTask(异步任务)
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); } // 后台解码图片 @Override protected Bitmap doInBackground(Integer... params) { data = params[0]; return decodeSampledBitmapFromResource(getResources(), data, 100, 100)); } // doInBackground执行完后, 检查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异步任务时,不阻止android虚拟机对这个ImageView对象的垃圾回收。因此,异步任务执行完后不保证这个ImageView对象没被垃圾回收掉,所以要在onPostExecute()方法中先检查一下这个对象是否是null。在异步任务执行完之前,用户浏览完了当前的activity,或者UI的配置改变了,都可能使ImageView不再存在。
有了上面的代码,异步的加载图片就很简单了。
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
- 处理异步并发
像ListView和GridView这样的组件,按照上述的异步加载图片,会涉及到多个异步任务的并发问题。为了更有效的利用内存,当用户上下卷动屏幕时,这样的组件会循环的利用那些已创建的子view。如果每个子view都关联一个AsyncTask异步任务,当异步任务完成后,异步任务所关联可能尚未被循环使用到在另一个子view中。而且,不能保证多个异步任务开始的顺序和这些异步任务结束的顺序保持一致。在博文《Multithreading for Performance》中,进一步讨论了并发的处理,并给出了一个解决方案:在ImageView组件中存储一个指向最近最新AsyncTask异步任务的引用,然后在一段时间后检查这个异步任务是否完成。这里也可以遵循上述解决方案的模式。
创建一个Drawable类的子类,其中存储一个指向BitmapWorkerTask的弱引用。在本实例中,BitmapDrawable对象用于在BitmapWorkerTask完成后使一个占位符图片显示在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)) {//这里cancelPotentialWork方法是实现异步任务并发的关键检测方法 final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), mPlaceHolderBitmap, task); imageView.setImageDrawable(asyncDrawable); task.execute(resId); } }
cancelPotentialWork()方法用于检测是否已经有另一个在运行的异步任务已经关联到该ImageView组件上。如果有,那么把已经关联的异步任务取消掉,通过调用cancel()方法。那么当cancelPotentialWork()方法执行的结果是false时,代表了新的异步任务的数据和已经关联到ImageView的异步任务的数据完全一样,这种情况比较少,但当出现在这种情况时,不需要做任何操作。
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);//获取到imageView所绑定的BitmapWorkerTask if (bitmapWorkerTask != null) { final int bitmapData = bitmapWorkerTask.data; if (bitmapData != data) { // 取消之前的任务 bitmapWorkerTask.cancel(true); } else { // 相同的任务,什么也不需要做 return false; } } return true; }
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是否还存在,然后将位图设置到imageView中。既然在cancelPotentialWork()方法中可能会取消异步任务的执行,那么需要在onPostExecute()方法中对异步任务被取消的情况作一些处理。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { ... @Override protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null;//异步任务被取消后,把bitmap置为null } if (imageViewReference != null && bitmap != null) { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask && imageView != null) {//异步任务未被取消,且imageview绑定的异步任务就是当前BitmapWorkerTask,则设置imageView的位图 imageView.setImageBitmap(bitmap); } } } }上述的代码适用于ListView和GridView组件以及其他循环使用子view的组件。设置ImageView的位图时,只需要简单的调用loadBitmap()方法就可以。换成设置GridView的图片的话,这个调用应该在支持适配器的getView()方法中。