1. 程式人生 > >高效使用Bitmaps(二) 後臺載入Bitmap

高效使用Bitmaps(二) 後臺載入Bitmap

為什麼要在後臺載入Bitmap?

有沒有過這種體驗:你在Android手機上打開了一個帶有含圖片的ListView的頁面,用手猛地一劃,就見那ListView嘎嘎地卡,彷彿每一個新的Item都是頂著阻力蹦出來的一樣?看完這篇文章,你將學會怎樣避免這種情況的發生。

在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法可以把圖片載入到Bitmap中。但由於這些方法是耗時的,所以多數情況下,這些方法應該放在非UI執行緒中,否則將有可能導致介面的卡頓,甚至是觸發ANR。

一般情況下,網路圖片的載入必須放在後臺執行緒中;而本地圖片就可以根據實際情況自行決定了,如果圖片不多不大的話,也可以在UI執行緒中操作來圖個方便。至於谷歌官方的說法,是隻要是從硬碟或者從網路載入Bitmap,統統不應該在主執行緒中進行。

基礎操作:使用AsyncTask

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

以上程式碼摘自Android官方文件,是一個後臺載入Bitmap並在載入完成後自動將Bitmap設定到ImageView的AsyncTask的實現。有了這個AsyncTask之後,非同步載入Bitmap只需要下面的簡單程式碼:

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

然後,一句loadBitmap(R.id.my_image, mImageView) 就能實現本地圖片的非同步載入了。

併發操作:在ListView和GridView中進行後臺載入

在實際中,影響效能的往往是ListView和GridView這種包含大量圖片的控制元件。在滑動過程中,大量的新圖片在短時間內一起被載入,對於沒有進行任何優化的程式,卡頓現象必然會隨之而來。通過使用後臺載入Bitmap的方式,這種問題將被有效解決。具體怎麼做,我們來看看谷歌推薦的方法。

首先建立一個實現了Drawable介面的類,用來儲存AsyncTask的引用。在本例中,選擇了繼承BitmapDrawable,用來給ImageView設定一個預留的佔位圖,這個佔位圖用於在AsyncTask執行完畢之前的顯示。

static classAsyncDrawableextendsBitmapDrawable{
    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();
    }
}

接下來,和上面類似,依然是使用一個loadBitmap()方法來實現對圖片的非同步載入。不同的是,要在啟動AsyncTask之前,把AsyncTask傳給AsyncDrawable,並且使用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);
    }
}

然後在Adapter的getView()方法中呼叫loadBitmap()方法,就可以為每個Item中的ImageView進行圖片的動態載入了。

loadBitmap()方法的程式碼中有兩個地方需要注意:第一,cancelPotentialWork()這個方法,它的作用是進行兩項檢查:首先檢查當前是否已經有一個AsyncTask正在為這個ImageView載入圖片,如果沒有就直接返回true。如果有,再檢查這個Task正在載入的資源是否與自己正要進行載入的資源相同,如果相同,那就沒有必要再進行多一次的載入了,直接返回false;而如果不同(為什麼會不同?文章最後會有解釋),就取消掉這個正在進行的任務,並返回true。第二個需要注意的是,本例中的 BitmapWorkerTask 實際上和上例是有所不同的。這兩點我們分開說,首先我們看cancelPotentialWork()方法的程式碼:

publicstaticbooleancancelPotentialWork(int data, ImageView imageView){
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // 取消之前的任務
            bitmapWorkerTask.cancel(true);
        } else {
            // 相同任務已經存在,直接返回false,不再進行重複的載入
            return false;
        }
    }
    // 沒有Task和ImageView進行繫結,或者Task由於載入資源不同而被取消,返回true
    return true;
}

在cancelPotentialWork()的程式碼中,首先使用getBitmapWorkerTask()方法獲取到與ImageView相關聯的Task,然後進行上面所說的判斷。好,我們接著來看這個getBitmapWorkerTask()是怎麼寫的:

privatestatic 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;
}
從程式碼中可以看出,該方法通過imageView獲取到它內部的Drawable物件,如果獲取到了並且該物件為AsyncDrawable的例項,就呼叫這個AsyncDrawable的getBitmapWorkerTask()方法來獲取到它對應的Task,也就是通過一個ImageView->Drawable->AsyncTask的鏈來獲取到ImageView所對應的AsyncTask。

好的,cancelPotentialWork()方法分析完了,我們回到剛才提到的第二個點:BitmapWorkerTask類的不同。這個類的改動在於onPostExecute()方法,具體請看下面程式碼:

classBitmapWorkerTaskextendsAsyncTask<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);
            }
        }
    }
}
從程式碼中可以看出,在後臺載入完Bitmap之後,它 並不是直接把Bitmap設定給ImageView,而是先判斷這個ImageView對應的Task是不是自己 (為什麼會不同?文章最後會有解釋)。如果是自己,才會執行ImageView的setImageBitmap()方法。到此,一個併發的非同步載入ListView(或GridView)中圖片的實現全部完成。

延伸:文中兩個“為什麼會不同”的解答

首先,簡單說一下ListView中Item和Item對應的View的關係(GridView中同理)。假設一個ListView含有100項,那麼它的100個Item應該分別對應一個View用於顯示,這樣一共是100個View。但Android實際上並沒有這樣做。出於記憶體考慮,Android只會為螢幕上可見的每個Item分配一個View。使用者滑動ListView,當第一個Item移動到可視範圍外後,他所對應的View將會被系統分配給下一個即將出現的Item。

回到問題。

我們不妨假設螢幕上顯示了一個ListView,並且它最多能顯示10個Item,而使用者在最頂部的Item(不妨稱他為第1個Item)使用Task載入Bitmap的時候進行了滑動,並且直到第1個Item消失而第11個Item已經在螢幕底部出現的時候,這個Task還沒有載入完成。那麼此時,原先與第1個Item繫結的ImageView已經被重新繫結到了第11個Item上,並且第11個Item觸發了getItem()方法。在getItem()方法中,ImageView第二次使用Task為自己載入Bitmap,但這時它需要載入的圖片資源已經變了(由第1個Item對應的資源變成了第11個Item對應的資源),因此在cancelPotentialWork()方法執行時會判斷兩個資源不一致。這就是為什麼相同ImageView卻對應了不同的資源。

同理,一個Task持有了一個ImageView,但由於這個Task有可能已經過時,因此這個ImageView所對應的Task未必就是這個Task本身,也有可能是另一個更年輕的Task。