在子线程中处理图片


从SD卡或者是从网络(除了从内存中)加载图片时,我们都应该注意不应该在主线程即UI线程中去加载图片。加载图片需要花费的时间可能受很多因素的影响,如SD卡的读取速度、网络的状态、图片的尺寸、CPU的加载能力等,都可能影响到图片的加载。如果这其中任何一个过程阻塞了UI线程,就会引发ANR,导致用户关闭应用。


下面我们就来讲解如何利用AsycTask来处理图片。


使用AsyncTask


AsyncTask为异步加载任务提供了一套简便的方法,它封装了主线程开启子线程,在子线程中执行耗时操作,且在完成后将结果回传给UI线程这一过程。使用时,需要新建一个类继承自AsyncTask并重写它的几个方法。


下面的例子使用AsyncTask加载了一个大图片并显示在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的实例采用弱指针可以防止AsyncTask阻止ImageView及与ImageView相关的对象被回收。由于在任务结束后ImageView不能保证还在显示,所以在onPostExecute()方法中必须判断ImageView是否还存在。


加载图片时,只需要新建一个task并执行它:


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



并发处理


一些UI控件如ListView、GridView,在和AsyncTask结合使用的时候会产生新的问题。为了更高效的使用内存,这些控件在用户滚动过程中会不断回收子项的View。如果每个Item都触发一个AsyncTask,那么当该项对应的Task返回时,该项的View可能已经被回收或者复用了。另外,任务启动的顺序也无法保证跟任务结束的顺序一致。



新建一个Drawable的子类用来维护task的实例:


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方法是检查是否该张图片已经被绑定任务并且任务正在执行中。如果检查发现该图片已经绑定了任务且任务正在执行,则可以通过调用cancel()来取消任务。下面是该方法的实现:


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;
}

上面的方法中,getBitmapWorderTask(),用来得到与ImageView绑定的task:


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;
}



最后一步在BitmapWorderTask中的onPostExecute()中显示,这时需检查此任务是否已经被cancel了并当前任务是否与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就可以了。