Android学习路线(三十二)在非UI线程中处理Bitmap

如果图片源数据是从硬盘,网络或是其他非内存中读取的话,在高效地夹在大Bitmaps中讨论的BitmapFactory.decode*方法就不能在主线程(UI线程)中执行。这种数据加载进内存的时间是不可预测的,并且还要依赖各种其他因素(磁盘读取速度,网络速度,图片大小,CPU性能等等)。如果这些任务的其中之一阻塞了UI线程,那么系统就认为你的app处于无响应状态并且弹出对话框提示用户关闭app。

本课介绍如何使用 AsyncTask 在后台线程中处理bitmaps,同时粗略讲解如何处理并发事件。

使用AsyncTask

AsyncTask 类提供了一个简单的方法来在后台线程处理任务,并且能够将处理结果在UI线程中返回。要使用它,需创建一个它的子类,然后覆盖几个必要的方法。下面这个例子向你展示如何使用 AsyncTask 和decodeSampledBitmapFromResource()将大bitmap加载到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 在任务执行结束后仍然存在,我们在使用它之前需要对它进行检查。ImageView 有可能已经不存在了,例如,用户切换到其他的activity中又或者在任务结束前发生了配置改变(横竖屏切换)都可能导致上述情况。

要异步地加载这个bitmap,只需要简单地执行下面的代码:

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

处理并发

通常的视图组件像ListView 和 GridView,如果像上一节中示范的那样与 AsyncTask 一起使用会引出其他的问题。为了提高内存使用效率,这些组件在滚动过程中会重用它们的子View。如果每个子View都触发了一个 AsyncTask,那么这些AsyncTask结束后没有任何确保,它们之前关联的View可能已经被重用了。并且,AsyncTask开始的顺序并不意味着它们的结束顺训。

Multithreading for Performance 这片文章讨论了如何处理并发,并且提供了提供了一个解决方法:将ImageView的引用存储起来,然后等到 AsyncTask 结束后查找对应的引用。使用一个类似的方法,上一节使用的 AsyncTask 将会使用类似的模式进行简单的扩展。

创建一个专用的Drawable 子类来存储任务的引用。在这种情况下,这个BitmapDrawable 的作用是让一个占位的图片在task结束后能够被显示在这个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)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

cancelPotentialWork 和上面的代码示例对应,检查是否已经有一个运行中的task与此ImageView相关联。如果是,此方法尝试通过调用cancel()方法取消此任务。在少数情况下,新的task数据和已经存在的task匹配,即可直接返回。下面是cancelPotentialWork的示例代码:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == 0 || 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() 方法, 检查task是否被取消以及当前的task是否与跟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 了,即使它们重用子View。 只需要简单的在你为ImageView设置图片的地方调用一下 loadBitmap 方法。例如在ListView对应的Adapter的getView方法中调用。

你可能感兴趣的:(并发,bitmap,ListView,AsyncTask)