在UI线程以外处理图片

——Android官网原文翻译

在"高效的加载图片"课程中我们讨论了BitmapFactory.decode方法,但是如果图片资源是从网络或者外存(或者其他非内存的存储位置)读取 的,那么我们不应该在UI线程中执行加载图片的操作。因为图片加载的时间是不可预期的并由众多的因素所决定(外存储器或者网络的读取速度,图片的大小,CPU的性能,等等)。如果加载图片的操作阻塞了UI线程,系统会把您的应用程序标识为"未响应"并提示用户关闭它(详情请参见响应式设计)。

这一节将要介绍AsyncTask的使用,并介绍怎样处理并发问题。

1.使用AsyncTask

AsyncTasks类提供了简单的方式用来在后台线程执行一些任务,并把结果传递到到UI线程。要使用这个类,就要创建一个继承了该类的子类,并重写一些方法。这里有一个使用AsyncTask和decodeSampledBitmapFromResource()(见上一节高效的加载图片)加载大图片到ImageView的例子。

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引用的内容不被GC回收。我们这里并不能保证当任务结束的时候ImageView的引用仍然可用,所以您也必须在onPostExecute()中检查引用。举个例子,当用户从当前Activity跳转到其他Activity或者某个设置在任务结束前发生变化的话,这个ImageView可能会不在存在。

如果要一步的加载一张图片,只需创建一个AsyncTask对象并调用execute方法。

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

2处理并发

通常,当使用像ListView或者GridView这类UI组件同时的使用上一节的AsyncTask加载图片时会遇到问题。为了有效的使用内存,这些控件会在滚动时回收子View,如果其中的每一个View都触发了一个AsyncTask,那么我们不能保证这些AsyncTask已经完成了,那么该相关联的View实际上并没有被回收。此外,我们也不能保证多个AsyncTask结束时的顺序会和它们开始时的顺序一样。

"使用多线程提升性能"这篇博客深入的讨论了并发性,并且提出了一个解决方案——ImageView保留一个最近AsyncTask的引用,该AsyncTask在任务完成时可以被检查到。使用相同的方法,上一节中的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();
    }
}

在执行BitampWorkerTask之前,您需要创建一个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);
    }
}

示例代码中使用的cancelPotentailWork方法,用来检查是否有其他正在运行的任务关联了ImageView。如果存在,它将尝试使用cacel()方法取消上一个任务。在少数情况下,新的任务数据与已经存在的任务的数据一样的话,则什么都不需要发生。以下是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;
        }
    }
    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<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中像其他组件回收它们的子views一样使用了。当您想要让一张图片在ImageView中显示时,只需简单的调用loadBitmap即可。举个例子,如果用来在一个GridView中显示图片的话,那么这个方法可以在GridView的adapter的getView()方法中调用。

posted on 2013-09-10 18:02  nbPengPeng  阅读(310)  评论(0编辑  收藏  举报