標籤:ack static scale drawing UI 它的 odi hdp 開啟
背景
在某個版本應用上線後,偶然測得首頁佔用的記憶體非常的大而且一直不能回收掉,經過一輪的排查後最終確定是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載入記憶體佔用徹底分析