在 高效的加载大尺寸图片 中介绍的 用来解析图片的 BitmapFactory.decode*
函数,需要在非UI线程中调用。
如果是读取网络图片或者磁盘图片,在UI线程中可能会导致程序ANR;如果是解析已经在内存中的图片,则可以在UI线程中调用这些函数。
这节内容介绍如何使用AsyncTask
来处理图片,以及如何处理并发访问。
使用 AsyncTask
AsyncTask
类是一个在后台线程中处理任务,并把结果反馈给UI线程的工具类。使用该类,只需要继承她并且重写对应的函数即可。
下面的代码演示了如何用这个类来载入一个大尺寸图片并显示在
ImageView
中:
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(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);
}
}
}
}
引用到
ImageView
的 WeakReference
确保
AsyncTask
不会直接引用 ImageView
从而导致其无法被垃圾回收。
当解析完图片后,无法确保 ImageView
是否还存在,所以需要在 onPostExecute()
函数中检查下,如果用户在图片加载期间离开了当前界面,则当图片加载完后用来显示图片的 ImageView
可能已经不存在了。
只要创建该Task并执行即可开始异步加载图片了:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
处理并发访问
像 ListView
和 GridView
这种控件使用上面介绍的方式来载入图片可能会引入新的问题。为了提高内存的使用率,当用户做滚动操作的时候这些控件会重复利用子控件,如果每个子控件都触发一个AsyncTask
,当任务完成的时候 无法保证该子控件是否已经被重用了。甚至,这些任务开始的顺序和完成的顺序也是不一样的。
这篇博文Multithreading for Performance 进一步
讨论了如何处理并发操作,并且提供了一个解决方案:
ImageView
中保存了最近触发的AsyncTask
对象,当任务完成的时候可以用来检测该引用的对象。
使用相似的函数,在前面介绍的AsyncTask
对象可以使用相似的模式来扩展。
创建一个特殊的 Drawable
子类来保存载入图片的Task引用。
这里使用 BitmapDrawable
类,这样当任务完成的时候可以直接在
ImageView
中显示:
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference(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
函数用来检测是否已经有一个Task绑定到 ImageView
了。
如果已经有个Task了就尝试取消这个Task(调用 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 {
...
@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
控件中使用该图片加载任务了。只需要在您设置
ImageView
图片的地方调用
loadBitmap
函数即可。
例如:在 GridView
中可能需要在Adapter中的 getView()
函数中调用
loadBitmap
函数。
Read more: http://blog.chengyunfeng.com/?p=394#ixzz2Ww5pPAPc
Read more: http://blog.chengyunfeng.com/?p=394#ixzz2Ww5wXqK
J