[原创] 连载 2 - 深入讨论 Android 关于高效显示图片的问题 - 如何在非 UI 线程处理位图

  更加详细的说明,可以参阅如下官网地址:http://developer.android.com/training/building-graphics.html
  快速导航
  1. 如何高效的加载大位图。(如何解码大位图,避免超过每个应用允许使用的最大内存)http://yhz61010.iteye.com/blog/1848337
  2. 如何在非 UI 线程处理位图。(如何使用 AsyncTask 在后台线程处理位图及处理并发问题)http://yhz61010.iteye.com/blog/1848811
  3. 如何对位图进行缓存。(如何通过创建内存缓存和磁盘缓存来流畅的显示多张位图)http://yhz61010.iteye.com/blog/1849645
  4. 如何管理位图内存。(如何针对不同的 Android 版本管理位图内存)http://yhz61010.iteye.com/blog/1850232
  5. 如何在 UI 中显示位图。(如何通过 ViewPager 和 GridView 显示多张图片)http://yhz61010.iteye.com/blog/1852927
  如何在非 UI 线程处理位图?
  前一篇文章http://yhz61010.iteye.com/blog/1848337提到的 BitmapFactory.decode 方法,不应该在主 UI 线程被调用(除非位图来源是内存),因为加载位置的时间不可预知的,而且还依赖于很多其它因素(例如,磁盘或网络的读取时间,图片大小,CPU 功率等)。无论上述任何一个因素导致 UI 线程被阻塞,那么系统会将此程序标记成无响应状态,此时用户有权关闭该程序。
  本文将引导你如何使用 AsyncTask 在后台线程处理位图,并且将为你演示如何处理并发问题。
  *使用 AsyncTask

  AsyncTask 类让我们可以使用简单的方法就可以在后台线程执行一些任务,并在处理完成后将结果反馈到 UI 线程。使用该类,需要创建一个它的子类,并覆写一些方法即可。下面为大家演示如何使用 AsyncTask 和 decodeSampledBitmapFromResource() 为 ImageView 设置一张大图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(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 依然可用(例如,用户可能会在后台任务执行时离开了当前页面或者对页面进行了其它操作导致无法找到 ImageView),所以我们必须在 onPostExecute() 方法中,检查引用的可用性。
  只需向下面这样简单的创建一个任务,并执行该任务,就可以实现异步加载位图:

1
2
3
4
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}

  并发处理
  某些共通视图组件,例如 ListView 和 GridView,若像上面那样将它们和 AsyncTask 连用时,就会遇到新的问题。为了高效的利用内存,当用户在上述组件中进行滚动时,这些组件会重用其中的子视图。如果每个子视图触发一个 AsyncTask,那么我们无法保证在任务执行完成时,触发该任务的子视图没有被其它子视图使用。此处,我们也无法保证任务执行完成的顺序和调用的顺序的一致性。
  Multithreading for Performance 这篇文章(可能需要代理才能访问:http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html)深入的讨论了如何解决并发问题,并且提供了一种解决方法,就是在使用 ImageView 的地方,保存一个指向最近被使用的 AsyncTask 引用,当这个任务完成时,可以检查 AsyncTask。使用如之类似的方法,我们可以将前面使用过的 AsyncTask 进行扩展来达到我们的目前:

1
2
3
4
5
6
7
8
9
10
11
12
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 上:

1
2
3
4
5
6
7
8
9
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 方法用于检查是否有其它正在运行的任务关联到当前的 ImageView 上。如果有其它正在运行的任务关联到当前的 ImageView 上,那么就可以调用 cancel() 方法来尝试取消先前的任务。在某些情况下,新任务的中的 data 和已经存在的任务的 data 相同,此时就不需要做任何处理。下面是 cancelPotentialWork 方法的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
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 相匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 组件,以及任何其它会重复使用它们子视图的组件。具体做法也很简单,只需要在原来为 ImageView 设值的地方调用 loadBitmap 方法即可。例如,在实现 GridView 时,在返回的 adapter 中的 getView() 方法中调用上述方法即可。