在子线程中处理图片
从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就可以了。