Android 開發繞不過的坑:你的 Bitmap 究竟佔多大內存?

0、寫在前面
本文涉及到屏幕密度的討論,這裏先要搞清楚 DisplayMetrics 的兩個變量,摘錄官方文檔的解釋:


  • density:The logical density of the display. This is a scaling factor for the Density Independent Pixel unit, where one DIP is one pixel on an approximately 160 dpi screen (for example a 240x320, 1.5”x2” screen), providing the baseline of the system’s display. Thus on a 160dpi screen this density value will be 1; on a 120 dpi screen it would be .75; etc.
    This value does not exactly follow the real screen size (as given by xdpi and ydpi, but rather is used to scale the size of the overall UI in steps based on gross changes in the display dpi. For example, a 240x320 screen will have a density of 1 even if its width is 1.8”, 1.3”, etc. However, if the screen resolution is increased to 320x480 but the screen size remained 1.5”x2” then the density would be increased (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 有個方便的方法,

publicfinal int getByteCount() {
    // int result permits bitmaps up to 46,340 x 46,340
    returngetRowBytes() * getHeight();
}



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


舉個例子
一張 522x686 的 PNG 圖片,我把它放到 drawable-xxhdpi 目錄下,在三星s6上加載,佔用內存2547360B,就可以用這個方法獲取到。



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

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

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



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

nativeRowBytes 對應的函數如下:
Bitmap.cpp

staticjint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
     SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
     returnstatic_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{returnfRowBytes; }


SkBitmap.cpp

size_t SkBitmap::ComputeRowBytes(Config c, intwidth) {
    returnSkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
SkImageInfo.h
 
staticintSkColorTypeBytesPerPixel(SkColorType ct) {
   staticconstuint8_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));
   returngSize[ct];
}
 
staticinline size_t SkColorTypeMinRowBytes(SkColorType ct, intwidth) {
    returnwidth * 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

publicstaticBitmap decodeResourceStream(Resources res, TypedValue value,
    InputStream is, Rect pad, Options opts) {
 
//實際上,我們這裏的opts是null的,所以在這裏初始化。
if(opts == null) {
    opts = newOptions();
}
 
if(opts.inDensity == 0&& value != null) {
    finalintdensity = value.density;
    if(density == TypedValue.DENSITY_DEFAULT) {
        opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
    }elseif(density != TypedValue.DENSITY_NONE) {
        opts.inDensity = density; //這裏density的值如果對應資源目錄爲hdpi的話,就是240
    }
}
 
if(opts.inTargetDensity == 0&& res != null) {
//請注意,inTargetDensity就是當前的顯示密度,比如三星s6時就是640
    opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
 
returndecodeStream(is, pad, opts);
}



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

publicOptions() {
   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)) {
        constintdensity = env->GetIntField(options, gOptions_densityFieldID);//對應hdpi的時候,是240
        constinttargetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//三星s6的爲640
        constintscreenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
        if(density != 0&& targetDensity != 0&& density != screenDensity) {
            scale = (float) targetDensity / density;
        }
    }
}
 
constbool willScale = scale != 1.0f;
......
SkBitmap decodingBitmap;
if(!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) {
   returnnullObjectReturn("decoder->decode returned false");
}
//這裏這個deodingBitmap就是解碼出來的bitmap,大小是圖片原始的大小
intscaledWidth = decodingBitmap.width();
intscaledHeight = decodingBitmap.height();
if(willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
    scaledWidth = int(scaledWidth * scale + 0.5f);
    scaledHeight = int(scaledHeight * scale + 0.5f);
}
if(willScale) {
    constfloatsx = scaledWidth / float(decodingBitmap.width());
    constfloatsy = 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)) {
        returnnullObjectReturn("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 = newOptions();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);




3.3 使用矩陣
用到 Bitmap 的地方,總會見到 Matrix。這時候你會想到什麼?
『基友』
『是在下輸了。。』
其實想想,Bitmap 的像素點陣,還不就是個矩陣,真是你中有我,我中有你的交情啊。那麼什麼時候用矩陣呢?

大圖小用用採樣,小圖大用用矩陣。
還是用前面模糊圖片的例子,我們不是採樣了麼?內存是小了,可是圖的尺寸也小了啊,我要用 Canvas 繪製這張圖可怎麼辦?當然是用矩陣了:
方式一:

Matrix matrix = newMatrix();
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);
 
    finalintnativeInt;
}


不過,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;
intbitDepth, 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)) {
#if0
    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 了。

staticbool canUpscalePaletteToConfig(SkColorType dstColorType, bool srcHasAlpha) {
  switch(dstColorType) {
    casekN32_SkColorType:
    casekARGB_4444_SkColorType:
        returntrue;
    casekRGB_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 = newOptions();
   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 引擎了。

怎麼辦,看來扔了好幾年的 C++還是要撿回來麼。。噗。。。



非常好的一篇乾貨,覺得很有用所以轉載了

原文地址(出處: Bugly)http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=498

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