1. 程式人生 > >Android Bitmap加載內存占用徹底分析

Android Bitmap加載內存占用徹底分析

android width rem alloc display may esp actor 緊急

背景

在某個版本應用上線後,偶然測得首頁占用的內存非常的大而且一直不能回收掉,經過一輪的排查後最終確定是3張圖片引起的!當時每張圖片占用了將近20m內存。當時緊急處理好後還一直惦記著此事,後來對Android加載Bitmap的內存占用作了徹底的分析,跟蹤了相關的源碼,在這裏總結一下。

圖片加載測試

先拋開結論,現在先直觀的看一下加載如下一張圖片需要多少內存

這裏寫圖片描述

其中圖片的寬高都為300像素

計算內存的方法采用 android.graphics.Bitmap#getByteCount

public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
1
2
3
4
預期占用的內存大小為

圖片寬*圖片高*表示每個像素點的字節數,即
1
這裏寫圖片描述

加載SD卡的圖片

加載SD中的圖片結果為

這裏寫圖片描述

assets的圖片

加載asset目錄中的圖片結果為

這裏寫圖片描述

加載Resources的圖片

drawable目錄

這裏寫圖片描述

drawable-mdpi目錄

這裏寫圖片描述

drawable-hdpi目錄

這裏寫圖片描述

drawable-xhdpi目錄

這裏寫圖片描述

drawable-xhhdpi目錄

這裏寫圖片描述

drawable-xhhhdpi目錄

這裏寫圖片描述

內存占用分析

理論上,300 * 300像素的圖片,默認以4byte表示1個像素的情況下,占用的內存為
300 * 300 * 4 = 360000 byte

但是,實際上,只有從SD卡、assets目錄、drawable-xhdpi目錄下加載圖片才等於理論數值,其他數值都不等!

等等!,從圖片的大小看,不等於理論值的圖片好像被放大或者縮小了?我們可以驗證一下,把圖片在內存中的實際寬高打印出來

SD卡的

這裏寫圖片描述

drawable-mdpi的

這裏寫圖片描述

發現沒有?在drawable-mdpi目錄中的圖片在加載內存中時的寬高都放大了兩倍!!
其實,加載在SD卡和assets目錄的圖片時,圖片的尺寸不會被改變,但是drawable-xxxdpi目錄的照片的尺寸會被改變,這裏篇幅所限,就不一一截圖了,想驗證的可以下載demo(文末給出鏈接)試驗一下。至於尺寸改變的原因,下文會討論,這裏賣個關子。

查看源碼

正所謂源碼面前,了無秘密,欲知原理,還須從源碼下手,首先查看BitmapFactory.java文件

BitmapFactory.decodeFile
BitmapFactory.decodeResourceStream
1
2
這兩個方法的重載函數最終都會調用到

private static native Bitmap nativeDecode www.yunduanpingtai.cn Stream(InputStream is, byte[] storage,
Rect padding, Options opts);
1
2
這是一個本地方法,其相關實現在

frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
打開文件,找到如下的方法,就是本地方法的實現

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
jobject padding, jobject options) {

jobject bitmap = NULL;
SkAutoTUnref<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

if (stream.get()) {
SkAutoTUnref<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Create(stream, www.rbuluoyl.cn/BYTES_TO_BUFFER));
SkASSERT(bufferedStream.get() != NULL);
bitmap = doDecode(env, bufferedStream, padding, options);
}
return bitmap;

抓住我們要看的部分,這裏還調用了doDecode方法,調到doDecode會發現,bitmap解碼的邏輯基本框架都在裏面了,分析清楚它的邏輯,我們就能找到答案,方法非常長,有200多行,我把枝幹提取出來,並加上註釋如下

static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {

int sampleSize = 1;

SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode;
SkColorType prefColorType = kN32_SkColorType;

bool doDither = true;
bool isMutable = false;
float scale = 1.0f;
bool preferQualityOverSpeed = false;
bool requireUnpremultiplied = false;

jobject javaBitmap = NULL;

if (options != NULL) {
//options是BitmapFactory.Options的java對象,這裏獲取該對象的成員變量值並賦值給本地代碼的變量,下面類似格式的方法調用作用相同
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
if (optionsJustBounds(env, options)) {
decodeMode = SkImageDecoder::kDecodeBounds_Mode;
}

// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);

jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
preferQualityOverSpeed = env->GetBooleanField(options,
gOptions_preferQualityOverSpeedFieldID);
requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
javaBitmap = env->GetObjectField(www.zzktv.cn options, gOptions_bitmapFieldID);

//java裏,inScaled默認true,所以這裏總是執行,除非手動設置為false
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);
//重點就是這裏了,density、targetDensity、screenDensity的值決定了是否縮放、以及縮放的倍數
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
}

const bool willScale = scale != 1.0f;

...省略若幹行

//真正的decode操作,decodingBitmap是解碼的的結果,但如果要縮放,則返回縮放後的bitmap,看後面的代碼
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
!= SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}

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

// update options (if any)
if (options != NULL) {
jstring mimeType = getMimeTypeString(env, decoder->getFormat());
if (env->ExceptionCheck()) {
return nullObjectReturn("OOM in getMimeTypeString()");
}
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
}

// if we‘re in justBounds mode, return now (skip the java bitmap)
if (decodeMode == SkImageDecoder::kDecodeBounds_Mode) {
return NULL;
}

...省略若幹行

//scale != 1.0f就縮放bitmap,縮放的步驟概擴起來就是申請縮放後的內存,然後把所有的bitmap信息記錄復制到outputBitmap變量上;否則直接復制decodingBitmap的內容
if (willScale) {
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
const float sx = scaledWidth /www.bomaoyuLe.cn 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);
} else {
outputBitmap->swap(decodingBitmap);
}

...省略若幹行

//後面的部分就是返回bitmap對象給java代碼了

if (javaBitmap != NULL) {
bool isPremultiplied = !requireUnpremultiplied;
GraphicsJNI::reinitBitmap(env, javaBitmap, outputBitmap, isPremultiplied);
outputBitmap->notifyPixelsChanged();
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}

int bitmapCreateFlags = 0x0;
if (isMutable) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Mutable;
if (!requireUnpremultiplied) bitmapCreateFlags |= GraphicsJNI::kBitmapCreateFlag_Premultiplied;

// now create the java bitmap
return GraphicsJNI::createBitmap(env, outputBitmap, javaAllocator.getStorageObj(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);

上面的解析能勾畫出大概的邏輯了,其中秘密就在這一小段

//java裏,inScaled默認true,所以這裏總是執行,除非手動設置為false
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);
//重點就是這裏了,density、targetDensity、screenDensity的值決定了是否縮放、以及縮放的倍數
if (density != 0 && targetDensity != 0 && density != screenDensity) {

可以看到,BitmapFactory.Options對象的inScaled、inDensity、inTargetDensity、screenDensity四個值共同決定了bitmap是否被縮放以及縮放的倍數。

下面回到java部分的代碼繼續分析

為什麽在drawable文件夾的圖片會被縮放而SD卡、assets的圖片不會

現在要解決這個問題就是要看BitmapFactory.Options對象的inScaled、inDensity、inTargetDensity、screenDensity四個值是怎樣被賦值了

之前提到過,inScaled默認值為true

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

decodeFile方法在調用本地方法前調用會decodeStream和decodeStreamInternal

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
// we don‘t throw in this case, thus allowing the caller to only check
// the cache, and not force the image to be decoded.
if (is == null) {
return null;
}

Bitmap bm = null;

Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
bm = decodeStreamInternal(is, outPadding, opts);
}

if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}

setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}

return bm;
}

private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) {
// ASSERT(is != null);
byte [] tempStorage = null;
if (opts != null) tempStorage = opts.inTempStorage;
if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
可以看到,如果opts直到調用本地方法之前也沒有並沒有改變,故加載SD卡的圖片和assets的圖片並不會被縮放(加載assets的圖片對應的本地方法為nativeDecodeAsset,最後都會調用doDecode)

decodeResource方法的調用棧為 decodeResource->decodeResourceStream->decodeStream,後面就跟之前的一樣了,其中decodeResourceStream方法如下

/**
* Decode a new Bitmap from an InputStream. This InputStream was obtained from
* resources, which we pass to be able to scale the bitmap accordingly.
*/
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) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}

if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}

return decodeStream(is, pad, opts);

分別是drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxhdpi目錄的dpi值,在這些目錄的圖片,加載的時候就會被附上對應的值。因為默認的值是DENSITY_MEDIUM,所以drawable目錄和drawable-mdpi的圖片縮放的大小是一樣的

小結

圖片被縮放的原因在於資源目錄對應著dpi,當加載資源的dpi和屏幕實際的dpi不一樣時,進行縮放以使資源顯示效果得到優化

圖片資源放置選擇

前文所述,當我們的圖片資源只有一張的時候,該放到哪個目錄?放到assets目錄似乎是最安全的,不會因圖片被放大造成OOM,也不會因圖片縮小失真。但是assets目錄的資源用起來不方便啊!我認為,在現在屏幕密度基本為720p以上的時代,如果UI設計師只提供了一張圖片,就放到xhdpi或者xxhdpi目錄吧,不然放在drawable目錄會被放大幾倍的

Android Bitmap加載內存占用徹底分析