1. 程式人生 > >關於Bitmap的記憶體,載入和回收等

關於Bitmap的記憶體,載入和回收等

Bitmap載入圖片

Bitmap的載入離不開BitmapFactory類,關於Bitmap官方介紹:
Creates Bitmap objects from various sources, including files, streams, and byte-arrays.

BitmapFactory類提供了四類方法用來載入Bitmap:

  1. decodeFile(),從檔案系統載入。
  2. decodeResource(),資原始檔中載入。
  3. decodeStream(),從輸入流載入。
  4. decodeByteArray(),從位元組陣列中載入。

注意:檢視原始碼可以發現,decodeFile()

decodeResource()間接呼叫decodeStream()

Bitmap的記憶體位置

在Android3.0之前:Bitmap的畫素資料存放在Native記憶體,而Bitmap物件本身則存放在Dalvik Heap中。

從Android3.0開始:Bitmap的記憶體就全部在Dalvik Heap裡了 。

Bitmap的記憶體回收

在Android3.0之前,需要使用Bitmap.recycle()進行Bitmap的記憶體回收。

從Android3.0開始,不需要手動回收Bitmap了。

Bitmap的記憶體複用

從Android3.0開始,在Bitmap中引入了一個新的欄位BitmapFactory.Options.inBitmap

,設定此欄位為true後,解碼方法會嘗試複用一張存在的Bitmap。這意味著Bitmap的記憶體被複用,避免了記憶體的回收及申請過程,顯然效能表現更佳。

Android4.4(API 19)之前只有格式為jpg、png,同等寬高(要求苛刻),inSampleSize為1的Bitmap才可以複用。

從Android4.4(API 19)開始被複用的Bitmap的記憶體大於需要新申請記憶體的Bitmap的記憶體就可以了。

使用快取

LruCache+DiskLruCache

出於對效能和app的考慮,我們肯定是想著第一次從網路中載入到圖片之後,能夠將圖片快取在記憶體和sd卡中,這樣,我們就不用頻繁的去網路中載入圖片,可以很好地控制記憶體問題。

一般都會考慮使用LruCache+DiskLruCache,LruCache作為Bitmap在記憶體中的存放容器,在sd卡則使用DiskLruCache來統一管理磁碟上的圖片快取

SoftReference+inBitmap

之前提到,可以採用LruCache作為存放Bitmap的容器,而在LruCache中有一個方法值得留意,那就是entryRemoved(),按照文件給出的說法,在LruCache容器滿了需要淘汰存放其中的物件騰出空間的時候會呼叫此方法。

  • 注意:這裡只是物件被淘汰出LruCache容器,但並不意味著物件的記憶體會立即被Dalvik虛擬機器回收掉

此時可以在此方法中將Bitmap使用SoftReference包裹起來,並用事先準備好的一個HashSet容器來存放這些即將被回收的Bitmap,有人會問,這樣存放有什麼意義?

之前我們提到將inmutable設定為true,Bitmap的記憶體可以被複用,當然肯定要滿足之前所說的條件。

解碼方法對圖片進行decode的時候會檢查記憶體中是否有可複用的Bitmap,避免我們頻繁地去SD卡上載入圖片而造成系統性能的下降,畢竟從直接從記憶體中複用要比在SD卡上進行IO操作的效率要高很多。

Bitmap的畫素格式

1.ALPHA_8:顏色資訊只由透明度組成,佔8位

2.ARGB_4444:顏色資訊由透明度與R(Red),G(Green),B(Blue)四部分組成,每個部分都佔4位,總共佔16位

3.ARGB_8888:顏色資訊由透明度與R(Red),G(Green),B(Blue)四部分組成,每個部分都佔8位,總共佔32位。是Bitmap預設的顏色配置資訊,也是最佔空間的一種配置。

4.RGB_565:顏色資訊由R(Red),G(Green),B(Blue)三部分組成,R佔5位,G佔6位,B佔5位,總共佔16位

通常我們優化Bitmap時,當需要做效能優化或者防止OOM,我們通常會使用RGB_565,因為ALPHA_8只有透明度,顯示一般圖片沒有意義,Bitmap.Config.ARGB_4444顯示圖片不清楚,Bitmap.Config.ARGB_8888佔用記憶體最多

Bitmap的記憶體計算

Bitmap類中有一個方法getByteCount()

    /**
     * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
     *
     * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
     * no longer be used to determine memory usage of a bitmap. See {@link
     * #getAllocationByteCount()}.</p>
     */
    public final int getByteCount() {
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

還有一個方法getAllocationByteCount()

    /**
     * Returns the size of the allocated memory used to store this bitmap's pixels.
     *
     * <p>This can be larger than the result of {@link #getByteCount()} if a bitmap is reused to
     * decode other bitmaps of smaller size, or by manual reconfiguration. See {@link
     * #reconfigure(int, int, Config)}, {@link #setWidth(int)}, {@link #setHeight(int)}, {@link
     * #setConfig(Bitmap.Config)}, and {@link BitmapFactory.Options#inBitmap
     * BitmapFactory.Options.inBitmap}. If a bitmap is not modified in this way, this value will be
     * the same as that returned by {@link #getByteCount()}.</p>
     *
     * <p>This value will not change over the lifetime of a Bitmap.</p>
     *
     * @see #reconfigure(int, int, Config)
     */
    public final int getAllocationByteCount() {
        if (mBuffer == null) {
            // native backed bitmaps don't support reconfiguration,
            // so alloc size is always content size
            return getByteCount();
        }
        return mBuffer.length;
    }

通過方法註釋我們可以瞭解到,getByteCount()代表儲存Bitmap的色素需要的最少記憶體,而getAllocationByteCount()代表在記憶體中為Bitmap分配的記憶體大小

其實getByteCount()方法是在API12加入的,代表儲存Bitmap的色素需要的最少記憶體。從API19開始getAllocationByteCount()方法代替了getByteCount()

一般情況下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap記憶體如果複用之後,兩者就不一樣了

通過複用Bitmap來解碼圖片,如果被複用的Bitmap的記憶體比待分配記憶體的Bitmap大,那麼getByteCount()表示新解碼圖片佔用記憶體的大小(並非實際記憶體大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實佔用的記憶體大小

那getByteCount()和getAllocationByteCount()值是怎麼計算出來的呢?

例子

下面就來舉個例子計算理論上Bitmap載入一張圖片時,所佔記憶體的大小,和getByteCount()的結果比較一下。

假設

一張畫素為522*686的PNG圖片,把它放到drawable-xxhdpi目錄下,在三星s6上載入,getByteCount()的結果是2547360B

推導

第一步

預設的畫素格式是ARGB_8888,之前已經說到了ARGB_8888格式下的一個畫素點佔用32位記憶體即4個位元組

所以結果是:int res = 522*686*41432368

顯然和答案不一樣啊。

第二步

假設中說把圖片放到drawable-xxhdpi目錄下,在三星s6上載入。並不是隨口一說的,它們也是影響Bitmap所佔記憶體大小的重要因素。

我們讀取的是drawable目錄下面的圖片,用的是decodeResource方法,該方法本質上就兩步:

  • 讀取原始資源,這個呼叫了Resource.openRawResource方法,這個方法呼叫完成之後會對TypedValue進行賦值,其中包含了原始資源的density等資訊。

  • 呼叫decodeResourceStream對原始資源進行解碼和適配。這個過程實際上就是原始資源的density到螢幕density的一個對映

原始資源的density其實取決於資源存放的目錄(比如xxhdpi對應的是480),而螢幕density的值是和裝置的硬體有關的,三星s6的值為640。載入時,原始的資源會自動進行縮放

所以結果是:int res = (522 * 640 / 480) * (686 * 640 / 480) * 42546432

第三步

好像還是差那麼一點,其實系統是進行了精度處理。

所以最終結果是:int res = (522 * 640 / 480f + 0.5) * (686 * 640 / 480f + 0.5) * 42547360

inScaled

上面說的縮放和一個引數inScaled有關:

    public static class Options {
        /**
         * Create a default Options object, which if left unchanged will give
         * the same result from the decoder as if null were passed.
         */
        public Options() {
            inDither = false;
            inScaled = true;
            inPremultiplied = true;
        }
        ……
    }

如果inScaled設定為true,就縮放。設定為false,則不進行縮放。預設的值從上面程式碼可以看到,就是true。

縮放的比例為inTargetDensity / inDensity

但是縮放也只針對資原始檔有效,對於其他來源的圖片不起效果,我們可以從原始碼中引數上的解釋得知:

        /**
         * The pixel density to use for the bitmap.  This will always result
         * in the returned bitmap having a density set for it (see
         * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}).  In addition,
         * if {@link #inScaled} is set (which it is by default} and this
         * density does not match {@link #inTargetDensity}, then the bitmap
         * will be scaled to the target density before being returned.
         * 
         * <p>If this is 0,
         * {@link BitmapFactory#decodeResource(Resources, int)}, 
         * {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
         * and {@link BitmapFactory#decodeResourceStream}
         * will fill in the density associated with the resource.  The other
         * functions will leave it as-is and no density will be applied.
         *
         * @see #inTargetDensity
         * @see #inScreenDensity
         * @see #inScaled
         * @see Bitmap#setDensity(int)
         * @see android.util.DisplayMetrics#densityDpi
         */
        public int inDensity;

其中有一句The other functions will leave it as-is and no density will be applied就是這個意思。

以上所說inDensity和inTargetDensity其實是DPI(dots per inch),關於DPI的概念請移步 全面理解Android中的Px,DPI,DIP,Density,Sp等概念

結論

Bitmap載入資原始檔在記憶體當中佔用的大小取決於以下三點:

  • 畫素格式,前面我們已經提到,如果是ARGB8888那麼就是一個畫素4個位元組,如果是RGB565那就是2個位元組。
  • 原始檔案存放的資源目錄(是hdpi還是xxhdpi)
  • 目標螢幕的DPI(同等條件下,紅米在資源方面消耗的記憶體肯定是要小於三星S6的)

Bitmap載入其他來源的圖片,就和畫素格式有關。

減少Bitmap記憶體佔用

合理選擇Bitmap的畫素格式

不需要透明度的情況下,我們通常使用RGB_565

使用取樣

inSampleSize的值必須大於1時才會有效果,且取樣率同時作用於寬和高。當inSampleSize=1時,取樣後的圖片為圖片的原始大小。

當inSampleSize=n時,取樣後的圖片的寬高均為原始圖片寬高的1/n,這時畫素為原始圖片的1/(n*n),佔用記憶體也為原始圖片的1/(n*n)。

inSampleSize的取值應該總為2的整數倍,否則會向下取整,取一個最接近2的整數倍,比如inSampleSize=3時,系統會取inSampleSize=2。

假設一張1024*1024,模式為ARGB_8888的圖片,inSampleSize=2,原始佔用記憶體大小是4MB,取樣後的圖片佔用記憶體大小就是(1024/2) * (1024/2 )* 4 = 1MB。

  • 圖片的質量壓縮:上述用inSampleSize壓縮是尺寸壓縮,Android中還有一種壓縮方式叫質量壓縮。質量壓縮是在保持畫素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,經過它壓縮的圖片檔案大小(kb)會有改變,但是匯入成Bitmap後佔得記憶體是不變的,寬高也不會改變。因為要保持畫素不變,所以它就無法無限壓縮,到達一個值之後就不會繼續變小了。顯然這個方法並不適用與縮圖,其實也不適用於想通過壓縮圖片減少記憶體的適用,僅僅適用於想在保證圖片質量的同時減少檔案大小的情況而已

使用矩陣

我們之前使用inSampleSize對圖片進行取樣,取樣之後記憶體是小了,可是圖的尺寸也小了,我們要用Canvas繪製原始大小的圖片該怎麼辦?就可以使用矩陣:

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0, 0);
canvas.drawBitmap(bitmap, matrix, paint);

這樣,繪製出來的圖就是放大以後的效果了,不過佔用的記憶體卻仍然是我們取樣出來的大小。

如果我要把圖片放到ImageView當中呢?一樣可以,請看:

Matrix matrix = new Matrix();
matrix.postScale(2, 2, 0, 0);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ScaleType.MATRIX);
imageView.setImageBitmap(bitmap);