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*686的PNG 圖片,我把它放到 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 引擎了。