1. 程式人生 > >Android學習筆記:高效載入大量Bitmap

Android學習筆記:高效載入大量Bitmap

許多情況下,我們的應用中需要的圖片大小總是小於圖片的原始大小如果我們不在載入之前做一些處理的話,那麼我們會遇到比如圖片資源佔用大量記憶體的狀況,所以通常在載入圖片之前,我們做一些裁剪工作:

一、讀取Bitmap的維度和型別

BitmapFactory類提供了一些資料解壓方法,比如:decodeByteArray()、decodeFile()、decodeResource()等等。為了以各種來源的圖片資源為基礎建立點陣圖(Bitmap),我們需要選擇最有效的解壓方法(具體詳見Android官方文件),同時要注意的是,這些方法都會嘗試為被構造的點陣圖申請記憶體資源,因此比較容易出現OutOfMemory異常。每種解壓方法都需要程式設計師自己宣告一個BitmapFactory.Options(這其實是一個類),比如設定其中的inJustDecodeBounds屬性為true,然後使用decode方法可以獲取原圖的size,同時會返回一個null的Bitmap物件,以及outWidth、outHeight、outMimeType屬性,這個方法對於我們讀取原圖的大小、型別十分方便,下面是示例:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

為了避免上面提到的異常,需要在使用decode方法之前確定原圖的大小,除非你十分確定原圖的大小不小於你想剪裁的大小,但這一般是不可能的

二、載入一個裁剪的點陣圖版本

現在圖片的維度(大小)已經知道了,為了告訴decode方法給原圖建立一個多大的子點陣圖,我們需要設定inSampleSize引數,這個引數同樣包含在BitmapFactory.Options物件之內,關於inSampleSize引數,做出如下說明:

1)引數為正,如果引數<=1,那麼子圖的最終裁剪大小即是原圖的大小(相當於未裁剪)

2)引數大於1,一般是2的倍數,那麼子圖的長寬都會變為原來的:1/inSampleSize,這裡要說明的是,即使我們為inSampleSize設定的引數大小不是2的倍數,它也會當成向下取正最接近的2的倍數,下面的例項就體現了這一點;

為什麼我們要做裁剪?比如有一張1024*768畫素的影象要被載入記憶體,然而最終你要用到的圖片大小其實只有128*96,那麼我們會浪費很大一部分記憶體,這顯然是沒有必要的,下面是一個例項:

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}
為了使用以上的方法,我們在裁剪之前,一定要記得使用inJustDecodeBounder來獲取原圖大小,在使用完之後,記得將inJustDecodeBounds置為false:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

在剪裁工作做完後,我們就可以將點陣圖載入ImageView了,比如我們設定期望大小為100*100:
mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

三、將點陣圖處理任務從UI執行緒中分離出來

在實際工程中,我們不可能將這個工作留在UI執行緒,如果我們的圖片處理過程需要花費一些時間,那麼這個時候我們的UI就會處於一個卡死的狀態,這顯然不會為使用者所接受,所以,為圖片處理另起一個執行緒就成了必須了。

Step1.使用AsyncTask

這個類提供了一個簡單地方法使得程式能夠在後臺執行一些工作,關於AsyncTask,更多的可以參考這裡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的資源回收,關於這一點,之後再講。但是這個不能夠保證ImageView在任務結束後依然存在,所以我們必須在onPostExecute()方法中確認這個ImageView的引用依然存在。ImageView消失的情況可能會是:使用者從當前活動離開,或者在任務完成之前發生了一些相關的配置變化,為了非同步載入點陣圖(Bitmap),我們可以這樣:
public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

Step2.點陣圖匯入的併發處理

ListView或者GridView中,會遇到需要在每個元素中載入點陣圖的情況,同時在使用者下滑動作發生後,有些這類控制元件會選擇回收子元素的資源,如果每個子元素都相應觸發一個AsyncTask,這就不能保證當這些執行緒的圖片載入任務完成時,相應的包含這個圖片的控制元件子元素到底有沒有完成資源回收,而且,也不能保證這些執行緒任務的執行順序。這篇博文MulTithreading for Performance更深入的解釋了併發處理的一些問題,同時也提供瞭解決方案。

這裡也給出一個解決方法:

我們可以宣告一個Drawable子類來儲存之前任務的引用,在這種情況下,使用BitmapDrawable以確保當任務結束時一個圖片佔位符可以在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關聯在一起了,如果已經關聯,那麼這個方法會通過呼叫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;
}

關於上面提到的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()方法,以至於能夠檢查執行緒任務是否被刪除同時當前任務又和一個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);
            }
        }
    }
}

以上方法也適用於其他和ListViewGridView的能夠回收子元素控制元件資源的控制元件,在ImageView中我們只要簡單的呼叫loadBitmap方法就好了,GridView的實現將會需要介面卡呼叫getView()方法。

【此篇仍在更新】