banner
jzman

jzman

Coding、思考、自觉。
github

Bitmap位圖採樣和記憶體計算

Android 開發中經常考慮的一個問題就是 OOM (Out Of Memory),也就是內存溢出,一方面大量加載圖片時有可能出現 OOM,通過採樣壓縮圖片可避免 OOM,另一方面,如一張 1024 x 768 像素的圖像被縮略顯示在 128 x 96 的 ImageView 中,這種做法顯然是不值得的,可通過採樣加載一個合適的縮小版本到內存中,以減小內存的消耗,Bitmap 的優化主要有兩個方面如下:

  1. 有效的處理較大的圖
  2. 快取位圖

這篇文章主要側重於如何有效的處理較大的位圖。

此外,在 Android 中按照位圖採樣的方法加載一個縮小版本到內存中應該考慮因素?

  1. 估計加載完整圖像所需要的內存
  2. 加載這個圖片所需的空間帶給其程序的其他內存需求
  3. 加載圖片的目標 ImageView 或 UI 組件的尺寸
  4. 當前設備的螢幕尺寸或密度。

本文內容如下:

  1. 位圖採樣
  2. Bitmap 內存計算
  3. 測試效果

位圖採樣#

圖像有不同的形狀和大小,讀取較大的圖片時會耗費內存。讀取一個位圖的尺寸和類型,為了從多種資源創建一個位圖,BitmapFactory 類提供了許多解碼的方法,根據圖像數據資源選擇最合適的解碼方法,這些方法試圖請求分配內存來構造位圖,因此很容易導致 OOM 異常。每種類型的解碼方法都有額外的特徵可以讓你通過 BitMapFactory.Options 類指定解碼選項。當解碼時設置 inJustDecodeBounds 為 true,可在不分配內存之前讀取圖像的尺寸和類型,下面的代碼實現了簡單的位圖採樣:

/**
  * 位圖採樣
  * @param res
  * @param resId
  * @return
  */
public Bitmap decodeSampleFromResource(Resources res, int resId){
    //BitmapFactory創建設置選項
    BitmapFactory.Options options = new BitmapFactory.Options();
    //設置採樣比例
    options.inSampleSize = 200;
    Bitmap bitmap = BitmapFactory.decodeResource(res,resId,options);
    return bitmap;
}

注意:其他 decode... 方法與 decodeResource 類似,這裡都以 decodeRedource 為例。

實際使用時,必須根據具體的寬高要求計算合適的 inSampleSize 來進行位圖的採樣,比如,將一個分辨率為 2048 x 1536 的圖像使用 inSampleSize 值為 4 去編碼產生一個 512 x 384 的圖像,這裡假設位圖配置為 ARGB_8888,載入到內存中僅僅是 0.75M 而不是原來的 12M,關於圖像所占內存的計算將在下文中介紹,下面是根據所需寬高進行計算採樣比例的計算方法:

/**
 * 1.計算位圖採樣比例
 *
 * @param option
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public int calculateSampleSize(BitmapFactory.Options option, int reqWidth, int reqHeight) {
    //獲得圖片的原寬高
    int width = option.outWidth;
    int height = option.outHeight;

    int inSampleSize = 1;
    if (width > reqWidth || height > reqHeight) {
        if (width > height) {
            inSampleSize = Math.round((float) height / (float) reqHeight);
        } else {
            inSampleSize = Math.round((float) width / (float) reqWidth);
        }
    }
    return inSampleSize;
}

/**
 * 2.計算位圖採樣比例
 * @param options
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public int calculateSampleSize1(BitmapFactory.Options options, int reqWidth, int reqHeight) {

    //獲得圖片的原寬高
    int height = options.outHeight;
    int width = options.outWidth;

    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
        // 計算出實際寬高和目標寬高的比率
        final int heightRatio = Math.round((float) height / (float) reqHeight);
        final int widthRatio = Math.round((float) width / (float) reqWidth);
        /**
         * 選擇寬和高中最小的比率作為inSampleSize的值,這樣可以保證最終圖片的寬和高
         * 一定都會大於等於目標的寬和高。
         */
        inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
    }
    return inSampleSize;
}

獲得採樣比例之後就可以根據所需寬高處理較大的圖片了,下面是根據所需寬高計算出來的 inSampleSize 對較大位圖進行採樣:

/**
 * 位圖採樣
 * @param resources
 * @param resId
 * @param reqWidth
 * @param reqHeight
 * @return
 */
public Bitmap decodeSampleFromBitmap(Resources resources, int resId, int reqWidth, int reqHeight) {
    //創建一個位圖工廠的設置選項
    BitmapFactory.Options options = new BitmapFactory.Options();
    //設置該屬性為true,解碼時只能獲取width、height、mimeType
    options.inJustDecodeBounds = true;
    //解碼
    BitmapFactory.decodeResource(resources, resId, options);
    //計算採樣比例
    int inSampleSize = options.inSampleSize = calculateSampleSize(options, reqWidth, reqHeight);
    //設置該屬性為false,實現真正解碼
    options.inJustDecodeBounds = false;
    //解碼
    Bitmap bitmap = BitmapFactory.decodeResource(resources, resId, options);
    return bitmap;
}

在解碼過程中使用了 BitmapFactory.decodeResource () 方法,具體如下:

/**
 * 解碼指定id的資源文件
 */
public static Bitmap decodeResource(Resources res, int id, BitmapFactory.Options opts) {
    ...
    /**
     * 根據指定的id打開數據流讀取資源,同時為TypeValue進行複製獲取原始資源的density等信息
     * 如果圖片在drawable-xxhdpi,那麼density為480dpi
     */
    is = res.openRawResource(id, value);
    //從輸入流解碼出一個Bitmap對象,以便根據opts縮放相應的位圖
    bm = decodeResourceStream(res, value, is, null, opts);
    ...
}

顯然真正解碼的方法應該是 decodeResourceStream () 方法,具體如下:

/**
 * 從輸入流中解碼出一個Bitmap,並對該Bitmap進行相應的縮放
 */
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
                     InputStream is, Rect pad, BitmapFactory.Options opts) {

    if (opts == null) {
        //創建一個默認的Option對象
        opts = new BitmapFactory.Options();
    }

    /**
     * 如果設置了inDensity的值,則按照設置的inDensity來計算
     * 否則將資源文件夾所表示的density設置inDensity
     */
    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    /**
     * 同理,也可以通過BitmapFactory.Option對象設置inTargetDensity
     * inTargetDensity 表示densityDpi,也就是手機的density
     * 使用DisplayMetrics對象.densityDpi獲得
     */
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    //decodeStream()方法中調用了native方法
    return decodeStream(is, pad, opts);
}

設置完 inDensity 和 inTargetDensity 之後調用了 decodeStream () 方法,該方法返回完全解碼後的 Bitmap 對象,具體如下:

/**
 * 返回解碼後的Bitmap,
 */
public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
    ...
    bm = nativeDecodeAsset(asset, outPadding, opts);
    //調用了native方法:nativeDecodeStream(is, tempStorage, outPadding, opts);
    bm = decodeStreamInternal(is, outPadding, opts);
    Set the newly decoded bitmap's density based on the Options
    //根據Options設置最新解碼的Bitmap
    setDensityFromOptions(bm, opts);
    ...
    return bm;
}

顯然,decodeStream () 方法主要調用了本地方法完成 Bitmap 的解碼,跟蹤源碼發現 nativeDecodeAsset () 和 nativeDecodeStream () 方法都調用了 dodecode () 方法,doDecode 方法關鍵代碼如下:

/**
 * BitmapFactory.cpp 源碼
 */
static jobject doDecode(JNIEnv*env, SkStreamRewindable*stream, jobject padding, jobject options) {
    ...
    if (env -> GetBooleanField(options, gOptions_scaledFieldID)) {
        const int density = env -> GetIntField(options, gOptions_densityFieldID);
        const int targetDensity = env -> GetIntField(options, gOptions_targetDensityFieldID);
        const int screenDensity = env -> GetIntField(options, gOptions_screenDensityFieldID);
        if (density != 0 && targetDensity != 0 && density != screenDensity) {
            //計算縮放比例
            scale = (float) targetDensity / density;
        }
    }
    ...
    //原始Bitmap
    SkBitmap decodingBitmap;
    ...

    //原始位圖的寬高
    int scaledWidth = decodingBitmap.width();
    int scaledHeight = decodingBitmap.height();

    //綜合density和targetDensity計算最終寬高
    if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
        scaledWidth = int(scaledWidth * scale + 0.5f);
        scaledHeight = int(scaledHeight * scale + 0.5f);
    }
    ...
    //x、y方向上的縮放比例,大概與scale相等
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());
    ...
    //將canvas放大scale,然後繪製Bitmap
    SkCanvas canvas (outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, & paint);
}

上面代碼能看到縮放比例的計算,以及 density 與 targetDensity 對 Bitmap 寬高的影響,實際上間接影響了 Bitmap 在所占內存的大小,這個問題會在下文中舉例說明,注意 density 與當前 Bitmap 所對應資源文件(圖片)的目錄有關,如有一張圖片位於 drawable-xxhdpi 目錄中,其對應的 Bitmap 的 density 為 480dpi,而 targetDensity 就是 DisPlayMetric 的 densityDpi,也就是手機螢幕代表的 density。那麼怎麼查看 Android 中本地的 native 方法的實現呢,鏈接如下:
BitmapFactory.cpp,直接搜索 native 方法的方法名即可,可以試一下咯。

Bitmap 內存計算#

首先貢獻一張大圖 6000 x 4000 , 圖片接近 12M,【點擊獲取】 當直接加載這張圖片到內存中肯定會發生 OOM,當然通過適當的位圖採樣縮小圖片可避免 OOM,那麼 Bitmap 所占內存又如何計算呢,一般情況下這樣計算:

BitmapMemory=widthPixheightPix4Bitmap Memory = widthPix * heightPix * 4

可使用 bitmap.getConfig () 獲取 Bitmap 的格式,這裡是 ARGB_8888 ,這種 Bitmap 格式下一個像素點占 4 個字節,所以要 x 4,如果將圖片放置在 Android 的資源文件夾中,計算方式如下:

scale=targetDensitydensitywidthPix=originalWidthscaleheightPix=orignalHeightscaleBitmapMemory=widthPixscaleheightPixscale4 scale = \frac{targetDensity}{density} widthPix = originalWidth * scale heightPix = orignalHeight * scale Bitmap Memory = widthPix * scale * heightPix * scale * 4

上述簡單總結了一下 Bitmap 所占內存的計算方式,驗證時可使用如下方法獲取 Bitmap 所占內存大小:

BitmapMemory=bitmap.getByteCount()BitmapMemory = bitmap.getByteCount()

由於選擇的這張圖片直接加載會導致 OOM,所以下文的事例中都是先採樣壓縮,然後再進行 Bitmap 所占內存的計算。

直接採樣#

這種方式就是直接指定採樣比例 inSampleSize 的值,然後先採樣然後計算採樣後的內存,這裡指定 inSampleSize 為 200。

  1. 將該圖片放在 drawable-xxhdpi 目錄中,此時 drawable-xxhdpi 所代表的 density 為 480 (density),我的手機螢幕所代表的 density 是 480 (targetDensity),顯然,此時 scale 為 1,當然首先對圖片進行採樣,然後將圖片加載到內存中,此時 Bitmap 所占內存內存為:
inSampleSize=200scale=targetDensitydensity=480480=1widthPix=orignalScalescale=60002001=30heightPix=orignalHeightscale=40002001=20BitmapMemory=widthPixheightPix4=30204=2400(Byte) inSampleSize = 200 scale = \frac{targetDensity}{density} = \frac{480}{480} = 1 widthPix = orignalScale*scale = \frac{6000}{200}*1 = 30 heightPix = orignalHeight*scale =\frac{4000}{200}*1 = 20 Bitmap Memory = widthPix * heightPix * 4 = 30 * 20 * 4 = 2400(Byte)
  1. 將圖片放在 drawable-xhdpi 目錄中,此時 drawable-xhdpi 所代表的 density 為 320,我的手機螢幕所代表的 density 是 480 (targetDensity),將圖片加載到內存中,此時 Bitmap 所代表的內存為:
inSampleSize=200scale=targetDensitydensity=480320widthPix=orignalWidthscale=6000200480320=45heightPix=orignalHeightscale=4000200480320=30BitmapMemory=widthPixscaleheightPixscale4=45304=5400(Byte)inSampleSize = 200 scale = \frac{targetDensity}{density} = \frac{480}{320} widthPix = orignalWidth*scale = \frac{6000}{200}*\frac{480}{320} = 45 heightPix = orignalHeight*scale = \frac{4000}{200}*\frac{480}{320} = 30 Bitmap Memory = widthPix*scale * heightPix*scale * 4 = 45*30*4 = 5400(Byte)

計算採樣#

這種方式就是根據請求的寬高計算合適的 inSampleSize,而不是隨意指定 inSampleSize,實際開發中這種方式最常用,這裡請求寬高為 100x100,具體 inSampleSize 計算在上文中已經說明。

  1. 將圖片放在 drawable-xxhdpi 目錄中,此時 drawable-xxhdpi 所代表的 density 為 480,我的手機螢幕所代表的 density 是 480 (targetDensity),將圖片加載到內存中,此時 Bitmap 所代表的內存為:
inSampleSize=4000100=40scale=targetDensitydensity=480480=1widthPix=orignalWidthscale=6000401=150heightPix=orignalHeightscale=4000401=100BitmapMemory=widthPixscaleheightPixscale4=60000(Byte)inSampleSize = \frac{4000}{100} = 40 scale = \frac{targetDensity}{density} = \frac{480}{480} = 1 widthPix = orignalWidth*scale = \frac{6000}{40} *1= 150 heightPix = orignalHeight*scale = \frac{4000}{40}*1 = 100 BitmapMemory = widthPix*scale*heightPix*scale*4 = 60000(Byte)
  1. 將圖片放在 drawable-xhdpi 目錄中,此時 drawable-xhdpi 所代表的 density 為 320,我的手機螢幕所代表的 density 是 480 (targetDensity),將圖片加載到內存中,此時 Bitmap 所代表的內存為:
inSampleSize=4000100=40scale=targetDensitydensity=480320widthPix=orignalWidthscale=600040480320=225heightPix=orignalHeightscale=400040480320=150BitmapMemory=widthPixheightPix4=2251504=135000(Byte)inSampleSize = \frac{4000}{100} = 40 scale = \frac{targetDensity}{density}= \frac{480}{320} widthPix = orignalWidth*scale = \frac{6000}{40}*\frac{480}{320}=225 heightPix = orignalHeight*scale=\frac{4000}{40}*\frac{480}{320}=150 BitmapMemory=widthPix*heightPix*4=225*150*4=135000(Byte)

位圖採樣及 Bitmap 在不同情況下所占內存的計算大概過程如上所述。

測試效果#

測試效果圖參考如下:

drawable-xhdpidrawable-xxhdpi

image
|
image

Bitmap 位圖採樣和內存計算到此結束。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。