性能優化專題十一--高效使用Bitmaps使用AsyncTask解碼圖片

 高效使用Bitmaps有什麼好處?

我們常常提到的“Android程序優化”,通常指的是性能和內存的優化,即:更快的響應速度,更低的內存佔用。Android程序的性能和內存問題,大部分都和圖片緊密相關,而圖片的加載在很多情況下很用到Bitmap(位圖)這個類。而由於Bitmap自身的特性(將每個像素的屬性全部保存在內存中),導致稍有不慎就會創建出一個佔用內存非常大的Bitmap對象,從而導致加載過慢,還會有內存溢出的風險。所以,Android程序要做優化,Bitmap的優化是必不可少的一步。

需要對Bitmap進行優化的情形

首先請看一行代碼:

mImageView.setImageResource(R.drawable.my_image);

這是一行從資源文件中加載圖片到ImageView的代碼。通常這段代碼沒什麼問題,但有些情況下,你需要對這段代碼進行優化。例如當圖片的尺寸遠遠大於ImageView的尺寸時,或者當你要在一個ListView或GridView中批量加載一些大小未知的圖片時。實際上,以上這行代碼會在運行時使用BitmapFactory.decodeStream()方法將資源圖片生成一個Bitmap,然後由這個Bitmap生成一個Drawable,最後再將這個Drawable設置到ImageView。由於在過程中生成了Bitmap,因此如果你使用的圖片過大,就會導致性能和內存佔用的問題。另外,需要優化的情形不止這一種,這裏就不再列舉。

下面分步說明使用代碼來減小Bitmap的尺寸從而達到減小內存佔用的方法:

1. 獲取原圖片尺寸

通常,我們使用BitmapFactory.decodeResource()方法來從資源文件中讀取一張圖片並生成一個Bitmap。但如果使用一個BitmapFactory.Options對象,並把該對象的inJustDecodeBounds屬性設置爲true,decodeResource()方法就不會生成Bitmap對象,而僅僅是讀取該圖片的尺寸和類型信息:

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;

2. 根據原圖尺寸和目標區域的尺寸計算出合適的Bitmap尺寸

BitmapFactory.Options類有一個參數inSampleSize,該參數爲int型,他的值指示了在解析圖片爲Bitmap時在長寬兩個方向上像素縮小的倍數。inSampleSize的默認值和最小值爲1(當小於1時,解碼器將該值當做1來處理),且在大於1時,該值只能爲2的冪(當不爲2的冪時,解碼器會取與該值最接近的2的冪)。例如,當inSampleSize爲2時,一個2000*1000的圖片,將被縮小爲1000*500,相應地,它的像素數和內存佔用都被縮小爲了原來的1/4:

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 原始圖片的寬高
    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;

        // 在保證解析出的bitmap寬高分別大於目標尺寸寬高的前提下,取可能的inSampleSize的最大值
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

3. 根據計算出的inSampleSize生成Bitmap 

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // 首先設置 inJustDecodeBounds=true 來獲取圖片尺寸
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // 計算 inSampleSize 的值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 根據計算出的 inSampleSize 來解碼圖片生成Bitmap
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

這裏有一點要注意,就是要在第二遍decode之前把inJustDecodeBounds設置回false。

4. 調用以上的decodeSampledBitmapFromResource方法,使用自定尺寸的Bitmap

如果你要將一張大圖設置爲一個100*100的縮略圖,執行以下代碼:

mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

到此,使用decodeResource()方法將一個大圖解析爲小尺寸bitmap的應用就完成了。同理,還可以使用decodeStream(),decodeFile()等方法做相同的事,原理是一樣的。

延伸:一個Bitmap到底佔用多大內存?系統給每個應用程序分配多大內存?

· Bitmap佔用的內存爲:像素總數 * 每個像素佔用的內存。在Android中,Bitmap有四種像素類型:ARGB_8888、ARGB_4444、ARGB_565、ALPHA_8,他們每個像素佔用的字節數分別爲4、2、2、1。因此,一個2000*1000的ARGB_8888類型的Bitmap佔用的內存爲2000*1000*4=8000000B=8MB。

· Android根據設備屏幕尺寸和dpi的不同,給系統分配的單應用程序內存大小也不同,具體如下表(表格取自Android 4.4 Compatibility Definition Document (CDD)):

 屏幕尺寸  DPI  應用內存
 small / normal / large  ldpi / mdpi  16MB
 small / normal / large  tvdpi / hdpi  32MB
 small / normal / large  xhdpi  64MB
 small / normal / large  400dpi  96MB
 small / normal / large  xxhdpi  128MB
 xlarge  mdpi  32MB
 xlarge  tvdpi / hdpi  64MB
 xlarge  xhdpi  128MB
 xlarge  400dpi  192MB
 xlarge  xxhdpi  256MB

 

爲什麼要在後臺加載Bitmap?

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

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

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

基礎操作:使用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);
            }
        }
    }
}

以上代碼摘自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 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();
    }
}

接下來,和上面類似,依然是使用一個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()方法的代碼:

public static boolean cancelPotentialWork(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()是怎麼寫的:

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

從代碼中可以看出,該方法通過imageView獲取到它內部的Drawable對象,如果獲取到了並且該對象爲AsyncDrawable的實例,就調用這個AsyncDrawable的getBitmapWorkerTask()方法來獲取到它對應的Task,也就是通過一個ImageView->Drawable->AsyncTask的鏈來獲取到ImageView所對應的AsyncTask。

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

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

從代碼中可以看出,在後臺加載完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。

完整圖片工具類:

package com.gala.video.app.opr.live.epg.news;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView;

import com.gala.video.lib.framework.core.env.AppRuntimeEnv;

import java.lang.ref.WeakReference;

public class BitmapWorkTask extends AsyncTask<Integer, Void, Bitmap> {

    private final WeakReference<ImageView> imageViewWeakReference;
    private int data = 0;

    public BitmapWorkTask(ImageView imageView) {
        imageViewWeakReference = new WeakReference<ImageView>(imageView);
    }

    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(AppRuntimeEnv.get().getApplicationContext().getResources(), data, 100, 100);
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewWeakReference != null && bitmap != null) {
            final ImageView imageView = imageViewWeakReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    private Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                         int reqWidth, int reqHeight) {

        // 首先設置 inJustDecodeBounds=true 來獲取圖片尺寸
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);

        // 計算 inSampleSize 的值
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

        // 根據計算出的 inSampleSize 來解碼圖片生成Bitmap
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    private int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 原始圖片的寬高
        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;

            // 在保證解析出的bitmap寬高分別大於目標尺寸寬高的前提下,取可能的inSampleSize的最大值
            while ((halfHeight / inSampleSize) > reqHeight
                    && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }

        return inSampleSize;
    }


}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章