在Android中高效的顯示圖片 - 加載大圖一文中講到了BitmapFactory.decode*方法的使用,但使用時需要注意不應該在UI線程中調用它們來從硬盤、網絡或者其他非內存的地方加載圖片。因為加載圖片所需要的時間是不可預測的,它跟很多因素有關,比如網絡狀況、硬盤讀寫速度、圖片的大小、CPU的速度等。如果我們阻塞UI線程來加載圖片有可能會導致ANR。
(本文出處:http://www.lxweimin.com/p/adf6c5cf4fbd)
使用AsyncTask加載圖片
解決方法就是開啟一個后臺線程來異步加載圖片。使用Android API提供的AsyncTask類可以很方便的在后臺線程中完成圖片加載然后將結果返回給UI線程。下面就是一個使用AsyncTask來異步加載圖片的例子。
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。例如,在任務完成之前用戶退出了這個Activity,那么這個ImageView是應該被釋放的。由于異步的關系我們無法保證任務執行完成后ImageView仍然存在,所以需要在onPostExecute方法里檢查ImageView的引用。
并發
例如ListView、GridView這類視圖組件結合上面的AsyncTask又會引入另一個問題。為了高效的使用內存,這類組件在滑動的時候會復用子view,如果一個view觸發一個AsyncTask,那我們無法保證任務執行完成后view沒有被復用。如果view被復用從而觸發兩次AsyncTask,我們也無法保證異步任務的執行順序,很有可能先觸發的任務后執行完成,這就會導致結果錯誤。
這里提供的解決方案是在ImageView中綁定最后觸發的AsyncTask的引用,當任務執行完成后返回結果時再比較返回結果的任務是不是ImageView綁定的任務,這樣就可以保證Imageview顯示的結果就是它最后觸發的AsyncTask的結果。
ImageView是系統的一個類,他并沒有給我們預設一個屬性讓我們來記錄AsyncTask,那么我們如何才能將AsyncTask綁定到ImageView中去呢?當然可以繼承ImageView來自定義一個包含AsyncTask字段的AsyncImageView,但是這樣可能會影響到布局文件。這里使用了另外一種實現方式。大家都知道ImageView有一個setImageDrawable(BitmapDrawable b)的方法,這就說明ImageView可以保存一個BitmapDrawable變量,如果我們能將AsyncTask放到BitmapDrawable,那么實際上AsyncTask也就放到ImageView里了。所以我們只需要繼承BitmapDrawable實現一個AsyncDrawable,把這個AsyncDrawable設置給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方法是用來檢查在當前的ImageView上是不是有正在運行的異步任務,如果有且上一個任務與當前請求的任務是同一個任務就直接返回false避免重復請求,如果有且任務不一樣就取消上一個任務。
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里獲取綁定的AsyncTask的方法。
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);
}
}
}
}
現在BitmapWorkerTask可用在ListView、GradView或者別的復用子view的組件上完美運行了。只需要在你想要給ImageView設置圖片的地方調用loadBitmap即可。例如,在GridView的適配器的getView方法里調用loadBitmap給子view設置圖片內容。
總結
本文實現了后臺線程加載圖片的功能。并對因多線程引入的并發問題給出了解決方案。
本文是《Android中高效的顯示圖片》專題中的第二篇
- Android中高效的顯示圖片 - 加載大圖
- Android中高效的顯示圖片 - 非UI線程加載
- Android中高效的顯示圖片 - 圖片緩存
- Android中高效的顯示圖片 - Bitmap的內存模型