Android 应用开发中不能在 UI 线程中做耗时的操作,否则就会弹出烦人的 ANR 窗口。
应用开发中如果需要加载来自网络、磁盘或其他非内存中图片资源时,因加载时间会受到其他因素(如磁盘、网络、图片大小、CPU 等等)的影响,很容易产生耗时操作。所以在进行类似操作时要避免在 UI 线程中进行。今天就和大家分享一下如何通过 AsyncTask 异步加载图片和怎么处理多线程并发问题。
如何使用 AsyncTask 加载图片?
通过 AysncTask 可以很容易的在启动后台线程加载资源,然后将结果返回到 UI 线程中。使用它时,需要创建它的子类并实现相应的方法,如下是一个通过 AysncTask 和 decodeSampledBitmapFromResource() 方法加载一张大图片到 ImageView 中的例子:
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(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);
}
}
}
}
使用 WeakReference 保存 ImageView 的原因,是为了在内存资源紧张时确保 AsyncTask 不会阻止对其进行资源回收,因此当 task 结束时不能保证 Imageview 还存在,所以你应该在 onPostExecute 中对它进行验证(本例中在 Task 结束前如果用户关闭 Activity,或系统设置改变时,ImageView 可能会被回收)。
通过以下方式我们就可以异步加载图片:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
如何处理并发操作?
常用的 View 组件中 像 ListView、GridView 等 为了高效实用内存,用户在进行 View 滚动操作时系统会对不再使用子 View 进行资源回收,,采用上面的方式进行图片加载时会引入另外一个问题。如果在每个子 View 中开启 AsyncTask,不能保证在任务完成时,相关的 View 是否已经被回收。此外,也不能保证他们加载完成的顺序
我们可以通过将 AsyncTask 的引用保存 ImageView 关联 Drawable 中,任务完成时检查引用是否存在.
创建一个专用的 Drawable 子类,存储工作任务线程的引用。这样在任务完成时即可将图片设置在 ImageView 中。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
在执行 BitmapTask 前,你可以创建 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 判断是否已经存在正在运行的任务绑定在 ImageView 中,若有,通过执行任务 cancel 方法取消它,当然这种情况不常发生。
下面是 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;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
下面是一个辅助方法,通过 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 {
...
@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 回收处理的组件中使用,通过调用
loadBitmap 你可以很简单的添加图片到 ImageView 中,如:在 GirdView 的 Adapter 中的 getView 方法中调用。