Android 性能優化(五)之細說 Bitmap

在上一篇《Android性能優化(四)以內存優化實戰》中談到那個內存中的大胖子Bitmap,Bitmap對內存的影響極大。javascript

例如:使用Pixel手機拍攝4048x3036像素(1200W)的照片,若是按ARGB_8888來顯示的話,須要48MB的內存空間(4048*3036*4 bytes),這麼大的內存消耗極易引起OOM。本篇文章就來講一說這個大胖子。html

1. Bitmap內存模型

Android Bitmap內存的管理隨着系統的版本迭代也有演進:java

1.在Android 2.2(API8)以前,當GC工做時,應用的線程會暫停工做,同步的GC會影響性能。而Android2.3以後,GC變成了併發的,意味着Bitmap沒有引用的時候其佔有的內存會很快被回收。android

2.在Android 2.3.3(API10)以前,Bitmap的像素數據存放在Native內存,而Bitmap對象自己則存放在Dalvik Heap中。Native內存中的像素數據並不會以可預測的方式進行同步回收,有可能會致使內存升高甚至OOM。而在Android3.0以後,Bitmap的像素數據也被放在了Dalvik Heap中。git

2. Bitmap的內存回收

2.1 Android2.3.3以前

在Android2.3.3以前推薦使用Bitmap.recycle()方法進行Bitmap的內存回收。github

備註:只有當肯定這個Bitmap不被引用的時候才能調用此方法,不然會有「Canvas: trying to use a recycled bitmap」這個錯誤。canvas

官方提供了一個使用Recycle的實例:使用引用計數來判斷Bitmap是否被展現或緩存,判斷可否被回收。數組

2.2 Android3.0以後

Android3.0以後,並無強調Bitmap.recycle();而是強調Bitmap的複用:緩存

2.2.1 Save a bitmap for later use

使用LruCache對Bitmap進行緩存,當再次使用到這個Bitmap的時候直接獲取,而不用重走編碼流程。性能優化

2.2.2 Use an existing bitmap

Android3.0(API 11以後)引入了BitmapFactory.Options.inBitmap字段,設置此字段以後解碼方法會嘗試複用一張存在的Bitmap。這意味着Bitmap的內存被複用,避免了內存的回收及申請過程,顯然性能表現更佳。不過,使用這個字段有幾點限制:

  • 聲明可被複用的Bitmap必須設置inMutable爲true;
  • Android4.4(API 19)以前只有格式爲jpg、png,同等寬高(要求苛刻),inSampleSize爲1的Bitmap才能夠複用;
  • Android4.4(API 19)以前被複用的Bitmap的inPreferredConfig會覆蓋待分配內存的Bitmap設置的inPreferredConfig;
  • Android4.4(API 19)以後被複用的Bitmap的內存必須大於須要申請內存的Bitmap的內存;
  • Android4.4(API 19)以前待加載Bitmap的Options.inSampleSize必須明確指定爲1。

3. Bitmap佔有多少內存?

3.1 getByteCount()

getByteCount()方法是在API12加入的,表明存儲Bitmap的色素須要的最少內存。API19開始getAllocationByteCount()方法代替了getByteCount()。

3.2 getAllocationByteCount()

API19以後,Bitmap加了一個Api:getAllocationByteCount();表明在內存中爲Bitmap分配的內存大小。

public final int getAllocationByteCount() {
        if (mBuffer == null) {
            //mBuffer表明存儲Bitmap像素數據的字節數組。
            return getByteCount();
        }
        return mBuffer.length;
    }複製代碼

3.3 getByteCount()與getAllocationByteCount()的區別

  • 通常狀況下二者是相等的;
  • 經過複用Bitmap來解碼圖片,若是被複用的Bitmap的內存比待分配內存的Bitmap大,那麼getByteCount()表示新解碼圖片佔用內存的大小(並不是實際內存大小,實際大小是複用的那個Bitmap的大小),getAllocationByteCount()表示被複用Bitmap真實佔用的內存大小(即mBuffer的長度)。(見第5節的示例)。

4. 如何計算Bitmap佔用的內存?

還記得以前我曾言之鑿鑿的說:不考慮壓縮,只是加載一張Bitmap,那麼它佔用的內存 = width * height * 一個像素所佔的內存。
如今想來實在慚愧:說法也對,可是不全對,沒有說明場景,同時也忽略了一個影響項:Density。

4.1 BitmapFactory.decodeResource()

BitmapFactory.java
    public static Bitmap decodeResourceStream(Resources res, TypedValue value,InputStream is, Rect pad, Options opts) {
        if (opts == null) {
            opts = new Options();
        }
        if (opts.inDensity == 0 && value != null) {
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
                //inDensity默認爲圖片所在文件夾對應的密度
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
                opts.inDensity = density;
            }
        }
        if (opts.inTargetDensity == 0 && res != null) {
            //inTargetDensity爲當前系統密度。
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        return decodeStream(is, pad, opts);
    }

    BitmapFactory.cpp 此處只列出主要代碼。
    static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
        //初始縮放係數
        float scale = 1.0f;
        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;
        if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
                != SkImageDecoder::kSuccess) {
            return nullObjectReturn("decoder->decode returned false");
        }
        //原始解碼出來的Bitmap的寬高;
        int scaledWidth = decodingBitmap.width();
        int scaledHeight = decodingBitmap.height();
        //要使用縮放係數進行縮放,縮放後的寬高;
        if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
            scaledWidth = int(scaledWidth * scale + 0.5f);
            scaledHeight = int(scaledHeight * scale + 0.5f);
        }    
        //源碼解釋爲由於歷史緣由;sx、sy基本等於scale。
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());
        canvas.scale(sx, sy);
        canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
        // now create the java bitmap
        return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
            bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
    }複製代碼

此處能夠看出:加載一張本地資源圖片,那麼它佔用的內存 = width * height * nTargetDensity/inDensity * nTargetDensity/inDensity * 一個像素所佔的內存。

實驗:將長爲102四、寬爲594的一張圖片放在xhdpi的文件夾下,使用魅族MX3手機加載。

// 不作處理,默認縮放。
        BitmapFactory.Options options = new BitmapFactory.Options();
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options);
        Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
        Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
        Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);

        Log.i(TAG,"===========================================================================");

        // 手動設置inDensity與inTargetDensity,影響縮放比例。
        BitmapFactory.Options options_setParams = new BitmapFactory.Options();
        options_setParams.inDensity = 320;
        options_setParams.inTargetDensity = 320;
        Bitmap bitmap_setParams = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options_setParams);
        Log.i(TAG, "bitmap_setParams:ByteCount = " + bitmap_setParams.getByteCount() + ":::bitmap_setParams:AllocationByteCount = " + bitmap_setParams.getAllocationByteCount());
        Log.i(TAG, "width:" + bitmap_setParams.getWidth() + ":::height:" + bitmap_setParams.getHeight());
        Log.i(TAG, "inDensity:" + options_setParams.inDensity + ":::inTargetDensity:" + options_setParams.inTargetDensity);

        輸出:
        I/lz: bitmap:ByteCount = 4601344:::bitmap:AllocationByteCount = 4601344
        I/lz: width:1408:::height:817 // 能夠看到此處:Bitmap的寬高被縮放了440/320=1.375倍
        I/lz: inDensity:320:::inTargetDensity:440 // 默認資源文件所處文件夾密度與手機系統密度
        I/lz: ===========================================================================
        I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
        I/lz: width:1024:::height:594 // 手動設置了縮放係數爲1,Bitmap的寬高都不變
        I/lz: inDensity:320:::inTargetDensity:320複製代碼

能夠看出:

  1. 不使用Bitmap複用時,getByteCount()與getAllocationByteCount()的值是一致的;
  2. 默認狀況下使用魅族MX三、在xhdpi的文件夾下,inDensity爲320,inTargetDensity爲440,內存大小爲4601344;而4601344 = 1024 * 594 * (440 / 320)* (440 / 320)* 4。
  3. 手動設置inDensity與inTargetDensity,使其比例爲1,內存大小爲2433024;2433024 = 1024 * 594 * 1 * 1 * 4。

4.2 BitmapFactory.decodeFile()

與BitmapFactory.decodeResource()的調用鏈基本一致,可是少了默認設置density和inTargetDensity(與縮放比例相關)的步驟,也就沒有了縮放比例這一說。

除了加載本地資源文件的解碼方法會默認使用資源所處文件夾對應密度和手機系統密度進行縮放以外,別的解碼方法默認都不會。此時Bitmap默認佔用的內存 = width * height * 一個像素所佔的內存。這也就是上面4.1開頭講的須要注意場景。

4.3 一個像素佔用多大內存?

Bitmap.Config用來描述圖片的像素是怎麼被存儲的?
ARGB_8888: 每一個像素4字節. 共32位,默認設置。
Alpha_8: 只保存透明度,共8位,1字節。
ARGB_4444: 共16位,2字節。
RGB_565:共16位,2字節,只存儲RGB值。

5. Bitmap如何複用?

在上述2.2.2咱們談到了Bitmap的複用,以及複用的限制,Google在《Managing Bitmap Memory》中給出了詳細的複用Demo:

  1. 使用LruCache和DiskLruCache作內存和磁盤緩存;
  2. 使用Bitmap複用,同時針對版本進行兼容。
    此處我寫一個簡單的demo,機型魅族MX3,系統版本API21;圖片寬102四、高594,進行Bitmap複用的實驗;
BitmapFactory.Options options = new BitmapFactory.Options();
// 圖片複用,這個屬性必須設置;
options.inMutable = true;
// 手動設置縮放比例,使其取整數,方便計算、觀察數據;
options.inDensity = 320;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap, options);
// 對象內存地址;
Log.i(TAG, "bitmap = " + bitmap);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());

// 使用inBitmap屬性,這個屬性必須設置;
options.inBitmap = bitmap;
options.inDensity = 320;
// 設置縮放寬高爲原始寬高一半;
options.inTargetDensity = 160;
options.inMutable = true;
Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap_reuse, options);
// 複用對象的內存地址;
Log.i(TAG, "bitmapReuse = " + bitmapReuse);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());

輸出:
I/lz: bitmap = android.graphics.Bitmap@35ac9dd4
I/lz: width:1024:::height:594
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse = android.graphics.Bitmap@35ac9dd4 // 兩個對象的內存地址一致
I/lz: width:512:::height:297
I/lz: bitmap:ByteCount = 608256:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse:ByteCount = 608256:::bitmapReuse:AllocationByteCount = 2433024 // ByteCount比AllocationByteCount小複製代碼

能夠看出:

  1. 從內存地址的打印能夠看出,兩個對象實際上是一個對象,Bitmap複用成功;
  2. bitmapReuse佔用的內存(608256)正好是bitmap佔用內存(2433024)的四分之一;
  3. getByteCount()獲取到的是當前圖片應當所佔內存大小,getAllocationByteCount()獲取到的是被複用Bitmap真實佔用內存大小。雖然bitmapReuse的內存只有608256,可是由於是複用的bitmap的內存,於是其真實佔用的內存大小是被複用的bitmap的內存大小(2433024)。這也是getAllocationByteCount()可能比getByteCount()大的緣由。

6. Bitmap如何壓縮?

6.1 Bitmap.compress()

質量壓縮:
它是在保持像素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,不會減小圖片的像素。進過它壓縮的圖片文件大小會變小,可是解碼成bitmap後佔得內存是不變的。

6.2 BitmapFactory.Options.inSampleSize

內存壓縮:

  • 解碼圖片時,設置BitmapFactory.Options類的inJustDecodeBounds屬性爲true,能夠在Bitmap不被加載到內存的前提下,獲取Bitmap的原始寬高。而設置BitmapFactory.Options的inSampleSize屬性能夠真實的壓縮Bitmap佔用的內存,加載更小內存的Bitmap。
  • 設置inSampleSize以後,Bitmap的寬、高都會縮小inSampleSize倍。例如:一張寬高爲2048x1536的圖片,設置inSampleSize爲4以後,實際加載到內存中的圖片寬高是512x384。佔有的內存就是0.75M而不是12M,足足節省了15倍。

備註:inSampleSize值的大小不是隨便設、或者越大越好,須要根據實際狀況來設置。
如下是設置inSampleSize值的一個示例:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
    // 設置inJustDecodeBounds屬性爲true,只獲取Bitmap原始寬高,不分配內存;
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 計算inSampleSize值;
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 真實加載Bitmap;
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

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;
        // 寬和高比須要的寬高大的前提下最大的inSampleSize
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }
    return inSampleSize;
}複製代碼

這樣使用:mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

備註:

  • inSampleSize比1小的話會被當作1,任何inSampleSize的值會被取接近2的冪值。

7. 總結

1. Bitmap內存模型

  • Android 2.3.3(API10)以前,Bitmap的像素數據存放在Native內存,而Bitmap對象自己則存放在Dalvik Heap中。而在Android3.0以後,Bitmap的像素數據也被放在了Dalvik Heap中。

2. Bitmap的內存回收

  • 在Android2.3.3以前推薦使用Bitmap.recycle()方法進行Bitmap的內存回收;
  • 在Android3.0以後更注重對Bitmap的複用;

3. Bitmap佔用內存的計算

  • getByteCount()方法是在API12加入的,表明存儲Bitmap的色素須要的最少內存;
    • getAllocationByteCount()在API19加入,表明在內存中爲Bitmap分配的內存大小;
  • 在複用Bitmap的狀況下,getAllocationByteCount()可能會比getByteCount()大;
  • 計算公式:
    • 對資源文件:width * height * nTargetDensity/inDensity * nTargetDensity/inDensity * 一個像素所佔的內存;
    • 別的:width * height * 一個像素所佔的內存;

4. Bitmap的複用

  • BitmapFactory.Options.inBitmap,針對不一樣版本複用有不一樣的限制,見上2.2.2,較多此處再也不贅述;

5. Bitmap的壓縮

  • Bitmap.compress(),質量壓縮,不會對內存產生印象;
  • BitmapFactory.Options.inSampleSize,內存壓縮;
    • inSampleSize的比對獲取;

6. Glide

  • 查看官方文檔以及性能優化典範,Google強烈推薦使用Glide來作Bitmap的加載。

參考:

歡迎關注微信公衆號:按期分享Java、Android乾貨!

歡迎關注
相關文章
相關標籤/搜索