1. 程式人生 > >Bitmap 究竟佔多大記憶體?

Bitmap 究竟佔多大記憶體?

  • reased (probably to 1.5).

  • densityDpi:The screen density expressed as dots-per-inch.

簡單來說,可以理解為 density 的數值是 1dp=density px;densityDpi 是螢幕每英寸對應多少個點(不是畫素點),在 DisplayMetrics 當中,這兩個的關係是線性的:

density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640

為了不引起混淆,本文所有提到的密度除非特別說明,都指的是 densityDpi,當然如果你願意,也可以用 density 來說明問題。

另外,本文的依據主要來自 android 5.0 的原始碼,其他版本可能略有出入。文章難免疏漏,歡迎指正~

1、佔了多大記憶體?

做移動客戶端開發的朋友們肯定都因為圖頭疼過,說起來曾經還有過 leader 因為組裡面一哥們在工程裡面加了一張 jpg 的圖發脾氣的事兒,哈哈。

為什麼頭疼呢?吃記憶體唄,時不時還給你來個 OOM 沖沖喜,讓你的每一天過得有滋有味(真是沒救了)。那每次工程裡面增加一張圖片的時候,我們都需要關心這貨究竟要佔多大的坑,佔多大呢?Android API 有個方便的方法,

public final int getByteCount() {
    // int result permits bitmaps up to 46,340 x 46,340
    return getRowBytes() * getHeight();
}

通過這個方法,我們就可以獲取到一張 Bitmap 在執行時到底佔用多大記憶體了。

舉個例子

一張 522x686 PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上載入,佔用記憶體2547360B,就可以用這個方法獲取到。

2、給我一張圖我告訴你佔多大記憶體

每次都問 Bitmap 你到底多大啦。。感覺怪怪的,畢竟我們不能總是去問,而不去搞清楚它為嘛介麼大吧。能不能給它算個命,算算它究竟多大呢?當然是可以的,很簡單嘛,我們直接順藤摸瓜,找出真凶,哦不,找出答案。

2.1 getByteCount

getByteCount 的原始碼我們剛剛已經認識了,當我們問 Bitmap 大小的時候,這孩子也是先拿到出生年月日,然後算出來的,那麼問題來了,getHeight 就是圖片的高度(單位:px),getRowBytes 是什麼?

public final int getrowBytes() {
   if (mRecycled) {
          Log.w(TAG, "Called getRowBytes() on a recycle()'d bitmap! This is undefined behavior!");
   }
   return nativeRowBytes(mFinalizer.mNativeBitmap);
}

額,感覺太對了啊,要 JNI 了。由於在下 C++ 實在用得少,每次想起 JNI 都請想象腦門磕牆的場景,不過呢,毛爺爺說過,一切反動派都是紙老虎~與 nativeRowBytes 對應的函式如下:

Bitmap.cpp

static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
     SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
     return static_cast<jint>(bitmap->rowBytes());
}

等等,我們好像發現了什麼,原來 Bitmap 本質上就是一個 SkBitmap。。而這個 SkBitmap 也是大有來頭,不信你瞧:Skia。啥也別說了,趕緊瞅瞅 SkBitmap。

SkBitmap.h

/** Return the number of bytes between subsequent rows of the bitmap. */
size_t rowBytes() const { return fRowBytes; }

SkBitmap.cpp

size_t SkBitmap::ComputeRowBytes(Config c, int width) {
    return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
 }
SkImageInfo.h

static int SkColorTypeBytesPerPixel(SkColorType ct) {
   static const uint8_t gSize[] = {
    0,  // Unknown
    1,  // Alpha_8
    2,  // RGB_565
    2,  // ARGB_4444
    4,  // RGBA_8888
    4,  // BGRA_8888
    1,  // kIndex_8
  };
  SK_COMPILE_ASSERT(SK_ARRAY_COUNT(gSize) == (size_t)(kLastEnum_SkColorType + 1),
                size_mismatch_with_SkColorType_enum);

   SkASSERT((size_t)ct < SK_ARRAY_COUNT(gSize));
   return gSize[ct];
}

static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
    return width * SkColorTypeBytesPerPixel(ct);
}

好,跟蹤到這裡,我們發現 ARGB_8888(也就是我們最常用的 Bitmap 的格式)的一個畫素佔用 4byte,那麼 rowBytes 實際上就是 4*width bytes。

那麼結論出來了,一張 ARGB_8888 的 Bitmap 佔用記憶體的計算公式

bitmapInRam = bitmapWidth*bitmapHeight *4 bytes

說到這兒你以為故事就結束了麼?有本事你拿去試,算出來的和你獲取到的總是會差個倍數,為啥呢?

還記得我們最開始給出的那個例子麼?

一張522*686 PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上載入,佔用記憶體2547360B,就可以用這個方法獲取到。

然而公式計算出來的可是1432368B。。。

2.2 Density

知道我為什麼在舉例的時候那麼費勁的說放到xxx目錄下,還要說用xxx手機麼?你以為 Bitmap 載入只跟寬高有關麼?Naive。

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

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

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

原始資源的 density 其實取決於資源存放的目錄(比如 xxhdpi 對應的是480),而螢幕 density 的賦值,請看下面這段程式碼:

BitmapFactory.java

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
    InputStream is, Rect pad, Options opts) {

//實際上,我們這裡的opts是null的,所以在這裡初始化。
if (opts == null) {
    opts = new Options();
}

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; //這裡density的值如果對應資源目錄為hdpi的話,就是240
    }
}

if (opts.inTargetDensity == 0 && res != null) {
//請注意,inTargetDensity就是當前的顯示密度,比如三星s6時就是640
    opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}

return decodeStream(is, pad, opts);
}

我們看到 opts 這個值被初始化,而它的構造居然如此簡單:

public Options() {
   inDither = false;
   inScaled = true;
   inPremultiplied = true;
}

所以我們就很容易的看到,Option.inScreenDensity 這個值沒有被初始化,而實際上後面我們也會看到這個值根本不會用到;我們最應該關心的是什麼呢?是 inDensity 和 inTargetDensity,這兩個值與下面 cpp 檔案裡面的 density 和 targetDensity 相對應——重複一下,inDensity 就是原始資源的 density,inTargetDensity 就是螢幕的 density。

緊接著,用到了 nativeDecodeStream 方法,不重要的程式碼直接略過,直接給出最關鍵的 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);//對應hdpi的時候,是240
        const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的為640
        const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
        if (density != 0 && targetDensity != 0 && density != screenDensity) {
            scale = (float) targetDensity / density;
        }
    }
}

const bool willScale = scale != 1.0f;
......
SkBitmap decodingBitmap;
if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
   return nullObjectReturn("decoder->decode returned false");
}
//這裡這個deodingBitmap就是解碼出來的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);
}
if (willScale) {
    const float sx = scaledWidth / float(decodingBitmap.width());
    const float sy = scaledHeight / float(decodingBitmap.height());

    // TODO: avoid copying when scaled size equals decodingBitmap size
    SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
    // FIXME: If the alphaType is kUnpremul and the image has alpha, the
    // colors may not be correct, since Skia does not yet support drawing
    // to/from unpremultiplied bitmaps.
    outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
            colorType, decodingBitmap.alphaType()));
    if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
        return nullObjectReturn("allocation failed for scaled bitmap");
    }

    // If outputBitmap's pixels are newly allocated by Java, there is no need
    // to erase to 0, since the pixels were initialized to 0.
    if (outputAllocator != &javaAllocator) {
        outputBitmap->eraseColor(0);
    }

    SkPaint paint;
    paint.setFilterLevel(SkPaint::kLow_FilterLevel);

    SkCanvas canvas(*outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}
......
}

注意到其中有個 density 和 targetDensity,前者是 decodingBitmap 的 density,這個值跟這張圖片的放置的目錄有關(比如 hdpi 是240,xxhdpi 是480),這部分程式碼我跟了一下,太長了,就不列出來了;targetDensity 實際上是我們載入圖片的目標 density,這個值的來源我們已經在前面給出了,就是 DisplayMetrics 的 densityDpi,如果是三星s6那麼這個數值就是640。sx 和sy 實際上是約等於 scale 的,因為 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。我們看到 Canvas 放大了 scale 倍,然後又把讀到記憶體的這張 bitmap 畫上去,相當於把這張 bitmap 放大了 scale 倍。

再來看我們的例子:

一張522*686PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上載入,佔用記憶體2547360B,其中 density 對應 xxhdpi 為480,targetDensity 對應三星s6的密度為640:

522/480 * 640 * 686/480 *640 * 4 = 2546432B

2.3 精度

越來越有趣了是不是,你肯定會發現我們這麼細緻的計算還是跟獲取到的數值

不!一!樣!

為什麼呢?由於結果已經非常接近,我們很自然地想到精度問題。來,再把上面這段程式碼中的一句拿出來看看:

outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
            colorType, decodingBitmap.alphaType()));

我們看到最終輸出的 outputBitmap 的大小是scaledWidth*scaledHeight,我們把這兩個變數計算的片段拿出來給大家一看就明白了:

if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}

在我們的例子中,

scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696

scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915

下面就是見證奇蹟的時刻:

915 * 696 * 4 = 2547360

有木有很興奮!有木有很激動!!

寫到這裡,突然想起《STL原始碼剖析》一書的扉頁,侯捷先生只寫了一句話:

“原始碼之前,了無祕密”。

2.4 小結

其實,通過前面的程式碼跟蹤,我們就不難知道,Bitmap 在記憶體當中佔用的大小其實取決於:

  • 色彩格式,前面我們已經提到,如果是 ARGB8888 那麼就是一個畫素4個位元組,如果是 RGB565 那就是2個位元組

  • 原始檔案存放的資源目錄(是 hdpi 還是 xxhdpi 可不能傻傻分不清楚哈)

  • 目標螢幕的密度(所以同等條件下,紅米在資源方面消耗的記憶體肯定是要小於三星S6的)

3、想辦法減少 Bitmap 記憶體佔用

3.1 Jpg 和 Png

說到這裡,肯定會有人會說,我們用 jpg 吧,jpg 格式的圖片不應該比 png 小麼?

這確實是個好問題,因為同樣一張圖片,jpg 確實比 png 會多少小一些(甚至很多),原因很簡單,jpg 是一種有損壓縮的圖片儲存格式,而 png 則是 無損壓縮的圖片儲存格式,顯而易見,jpg 會比 png 小,代價也是顯而易見的。

可是,這說的是檔案儲存範疇的事情,它們只存在於檔案系統,而非記憶體或者視訊記憶體。說得簡單一點兒,我有一個極品飛車的免安裝硬碟版的壓縮包放在我的磁盤裡面,這個遊戲是不能玩的,我需要先解壓,才能玩——jpg 也好,png 也好就是個壓縮包的概念,而我們討論的記憶體佔用則是從使用角度來討論的。

所以,jpg 格式的圖片與 png 格式的圖片在記憶體當中不應該有什麼不同。

『啪!!!』

『誰這麼缺德!!打人不打臉好麼!』

肯定有人有意見,jpg 圖片讀到記憶體就是會小,還會給我拿出例子。當然,他說的不一定是錯的。因為 jpg 的圖片沒有 alpha 通道!!所以讀到記憶體的時候如果用 RGB565的格式存到記憶體,這下大小隻有 ARGB8888的一半,能不小麼。。。

不過,拋開 Android 這個平臺不談,從出圖的角度來看的話,jpg 格式的圖片大小也不一定比 png 的小,這要取決於影象資訊的內容:

JPG 不適用於所含顏色很少、具有大塊顏色相近的區域或亮度差異十分明顯的較簡單的圖片。對於需要高保真的較複雜的影象,PNG 雖然能無失真壓縮,但圖片檔案較大。

如果僅僅是為了 Bitmap 讀到記憶體中的大小而考慮的話,jpg 也好 png 也好,沒有什麼實質的差別;二者的差別主要體現在:

  • alpha 你是否真的需要?如果需要 alpha 通道,那麼沒有別的選擇,用 png。

  • 你的圖色值豐富還是單調?就像剛才提到的,如果色值豐富,那麼用jpg,如果作為按鈕的背景,請用 png。

  • 對安裝包大小的要求是否非常嚴格?如果你的 app 資源很少,安裝包大小問題不是很凸顯,看情況選擇 jpg 或者 png(不過,我想現在對資原始檔沒有苛求的應用會很少吧。。)

  • 目標使用者的 cpu 是否強勁?jpg 的影象壓縮演算法比 png 耗時。這方面還是要酌情選擇,前幾年做了一段時間 Cocos2dx,由於資源非常多,專案組要求統一使用 png,可能就是出於這方面的考慮。

嗯,跑題了,我們其實想說的是怎麼減少記憶體佔用的。。這一小節只是想說,休想通過這個方法來減少記憶體佔用。。。XD

3.2 使用 inSampleSize

有些朋友一看到這個肯定就笑了。取樣嘛,我以前是學訊號處理的,一看到 Sample 就抽抽。。哈哈開個玩笑,這個取樣其實就跟統計學裡面的取樣是一樣的,在保證最終效果滿足要求的前提下減少樣本規模,方便後續的資料採集和處理。

這個方法主要用在圖片資源本身較大,或者適當地取樣並不會影響視覺效果的條件下,這時候我們輸出地目標可能相對較小,對圖片解析度、大小要求不是非常的嚴格。

舉個例子

我們現在有個需求,要求將一張圖片進行模糊,然後作為 ImageView 的 src 呈現給使用者,而我們的原始圖片大小為 1080*1920,如果我們直接拿來模糊的話,一方面模糊的過程費時費力,另一方面生成的圖片又佔用記憶體,實際上在模糊運算過程中可能會存在輸入和輸出並存的情況,此時記憶體將會有一個短暫的峰值。

這時候你一定會想到三個字母在你的腦海裡揮之不去,它們就是『OOM』。

既然圖片最終是要被模糊的,也看不太情況,還不如直接用一張取樣後的圖片,如果取樣率為 2,那麼讀出來的圖片只有原始圖片的 1/4 大小,真是何樂而不為呢??

BitmapFactory.Options options = new Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);

3.3 使用矩陣

用到 Bitmap 的地方,總會見到 Matrix。這時候你會想到什麼?

『基友』

『是在下輸了。。』

其實想想,Bitmap 的畫素點陣,還不就是個矩陣,真是你中有我,我中有你的交情啊。那麼什麼時候用矩陣呢?

大圖小用用取樣,小圖大用用矩陣。

還是用前面模糊圖片的例子,我們不是取樣了麼?記憶體是小了,可是圖的尺寸也小了啊,我要用 Canvas 繪製這張圖可怎麼辦?當然是用矩陣了:

方式一:

Matrix matrix = new Matrix();
matrix.preScale(2, 2, 0f, 0f);
//如果使用直接替換矩陣的話,在Nexus6 5.1.1上必須關閉硬體加速
canvas.concat(matrix);
canvas.drawBitmap(bitmap, 0,0, paint);

需要注意的是,在使用搭載 5.1.1 原生系統的 Nexus6 進行測試時發現,如果使用 Canvas 的 setMatrix 方法,可能會導致與矩陣相關的元素的繪製存在問題,本例當中如果使用 setMatrix 方法,bitmap 將不會出現在螢幕上。因此請儘量使用 canvas 的 scale、rotate 這樣的方法,或者使用 concat 方法。

方式二:

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

3.4 合理選擇Bitmap的畫素格式

其實前面我們已經多次提到這個問題。ARGB8888格式的圖片,每畫素佔用 4 Byte,而 RGB565則是 2 Byte。我們先看下有多少種格式可選:

格式 描述
ALPHA_8 只有一個alpha通道
ARGB_4444 這個從API 13開始不建議使用,因為質量太差
ARGB_8888 ARGB四個通道,每個通道8bit
RGB_565 每個畫素佔2Byte,其中紅色佔5bit,綠色佔6bit,藍色佔5bit

這幾個當中,

ALPHA8 沒必要用,因為我們隨便用個顏色就可以搞定的。

ARGB4444 雖然佔用記憶體只有 ARGB8888 的一半,不過已經被官方嫌棄,失寵了。。『又要佔省記憶體,又要看著爽,臣妾做不到啊T T』。

ARGB8888 是最常用的,大家應該最熟悉了。

RGB565 看到這個,我就看到了資源優化配置無處不在,這個綠色。。(不行了,突然好邪惡XD),其實如果不需要 alpha 通道,特別是資源本身為 jpg 格式的情況下,用這個格式比較理想。

3.5 高能:索引點陣圖(Indexed Bitmap)

索引點陣圖,每個畫素只佔 1 Byte,不僅支援 RGB,還支援 alpha,而且看上去效果還不錯!等等,請收起你的口水,Android 官方並不支援這個。是的,你沒看錯,官方並不支援。

 public enum Config {
    // these native values must match up with the enum in SkBitmap.h

    ALPHA_8     (2),
    RGB_565     (4),
    ARGB_4444   (5),
    ARGB_8888   (6);

    final int nativeInt;
}

不過,Skia 引擎是支援的,不信你再看:

enum Config {
   kNo_Config,   //!< bitmap has not been configured
     kA8_Config,   //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque)

   //看這裡看這裡!!↓↓↓↓↓
    kIndex8_Config, //!< 8-bits per pixel, using SkColorTable to specify the colors  
    kRGB_565_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_4444_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing)
    kARGB_8888_Config, //!< 32-bits per pixel, (see SkColorPriv.h for packing)
    kRLE_Index8_Config,

    kConfigCount
};

其實 Java 層的列舉變數的 nativeInt 對應的就是 Skia 庫當中列舉的索引值,所以,如果我們能夠拿到這個索引是不是就可以了?對不起,拿不到。

不行了,廢話這麼多,肯定要挨板磚了T T。

不過呢,在 png 的解碼庫裡面有這麼一段程式碼:

 bool SkPNGImageDecoder::getBitmapColorType(png_structp png_ptr, png_infop info_ptr,
                                       SkColorType* colorTypep,
                                       bool* hasAlphap,
                                       SkPMColor* SK_RESTRICT theTranspColorp) {
png_uint_32 origWidth, origHeight;
int bitDepth, colorType;
png_get_IHDR(png_ptr, info_ptr, &origWidth, &origHeight, &bitDepth,
             &colorType, int_p_NULL, int_p_NULL, int_p_NULL);

#ifdef PNG_sBIT_SUPPORTED
  // check for sBIT chunk data, in case we should disable dithering because
  // our data is not truely 8bits per component
  png_color_8p sig_bit;
  if (this->getDitherImage() && png_get_sBIT(png_ptr, info_ptr, &sig_bit)) {
#if 0
    SkDebugf("----- sBIT %d %d %d %d\n", sig_bit->red, sig_bit->green,
             sig_bit->blue, sig_bit->alpha);
#endif
    // 0 seems to indicate no information available
    if (pos_le(sig_bit->red, SK_R16_BITS) &&
        pos_le(sig_bit->green, SK_G16_BITS) &&
        pos_le(sig_bit->blue, SK_B16_BITS)) {
        this->setDitherImage(false);
    }
}
#endif


if (colorType == PNG_COLOR_TYPE_PALETTE) {
    bool paletteHasAlpha = hasTransparencyInPalette(png_ptr, info_ptr);
    *colorTypep = this->getPrefColorType(kIndex_SrcDepth, paletteHasAlpha);
    // now see if we can upscale to their requested colortype
    //這段程式碼,如果返回false,那麼colorType就被置為索引了,那麼我們看看如何返回false
    if (!canUpscalePaletteToConfig(*colorTypep, paletteHasAlpha)) {
        *colorTypep = kIndex_8_SkColorType;
    }
} else {
...... 
}
return true;
}

canUpscalePaletteToConfig函式如果返回false,那麼colorType就被置為kIndex_8_SkColorType了。

static bool canUpscalePaletteToConfig(SkColorType dstColorType, bool srcHasAlpha) {
  switch (dstColorType) {
    case kN32_SkColorType:
    case kARGB_4444_SkColorType:
        return true;
    case kRGB_565_SkColorType:
        // only return true if the src is opaque (since 565 is opaque)
        return !srcHasAlpha;
    default:
        return false;
 }
}

如果傳入的 dstColorType kRGB_565_SkColorType,同時圖片還有 alpha 通道,那麼返回 false~~咳咳,那麼問題來了,這個dstColorType 是哪兒來的??就是我們在 decode 的時候,傳入的 Options inPreferredConfig

下面是實驗時間~

準備:在 assets 目錄當中放了一個叫 index.png 的檔案,大小192*192,這個檔案是通過 PhotoShop 編輯之後生成的索引格式的圖片。

程式碼

try {
   Options options = new Options();
   options.inPreferredConfig = Config.RGB_565;
Bitmap bitmap = BitmapFactory.decodeStream(getResources().getAssets().open("index.png"), null, options);
   Log.d(TAG, "bitmap.getConfig() = " + bitmap.getConfig());
   Log.d(TAG, "scaled bitmap.getByteCount() = " + bitmap.getByteCount());
   imageView.setImageBitmap(bitmap);
} catch (IOException e) {
    e.printStackTrace();
}

程式執行在 Nexus6上,由於從 assets 中讀取不涉及前面討論到的 scale 的問題,所以這張圖片讀到記憶體以後的大小理論值(ARGB8888):
192 * 192 *4=147456

好,執行我們的程式碼,看輸出的 Config 和 ByteCount:

D/MainActivity: bitmap.getConfig() = null
D/MainActivity: scaled bitmap.getByteCount() = 36864

先說大小為什麼只有 36864,我們知道如果前面的討論是沒有問題的話,那麼這次解碼出來的 Bitmap 應該是索引格式,那麼佔用的記憶體只有 ARGB 8888 的1/4是意料之中的;再說 Config 為什麼為 null。。額。。黑戶。。官方說:

public final Bitmap.Config getConfig ()

Added in API level 1

If the bitmap’s internal config is in one of the public formats, return that config, otherwise return null.

再說一遍,黑戶。。XD。

看來這個法子還真行啊,佔用記憶體一下小很多。不過由於官方並未做出支援,因此這個方法有諸多限制,比如不能在 xml 中直接配置,,生成的 Bitmap 不能用於構建 Canvas 等等。

3.6 不要辜負。。。『哦,不要姑父!』

其實我們一直在抱怨資源大,有時候有些場景其實不需要圖片也能完成的。比如在開發中我們會經常遇到 Loading,這些 Loading 通常就是幾幀圖片,圖片也比較簡單,只需要黑白灰加 alpha 就齊了。

『排期太緊了,這些給我出一系列圖吧』

『好,不過每張圖都是 300*30 0的 png 哈,總共 5 張,為了適配不同的解析度,需要出 xxhdpi 和 xxxhdpi 的兩套圖。。』

Orz。。。

如果是這樣,你還是自定義一個 View,覆寫 onDraw 自己畫一下好了。。。

4、結語

寫了這麼多,我們來稍稍理一理,本文主要討論瞭如何執行時獲取 Bitmap 佔用記憶體的大小,如果事先根據 Bitmap 的格式、讀取方式等算出其佔用記憶體的大小,後面又整理了一些常見的 Bitmap 使用建議。突然好像說,是時候研究一下 Skia 引擎了。