前置概念-螢幕密度
搞清楚 DisplayMetrics 的兩個變量,
density 是顯示的邏輯密度,是密度與獨立像素單元的比例因子,
densityDpi 是螢幕每英寸對應多少個點
關于DisplayMetrics更多細節點選這裡
圖檔占記憶體多少的計算原理
找到每個像素占用的位元組數*總像素數即可
Android API 有個友善的方法可以擷取到占用的記憶體大小
public final int getByteCount() {
// int result permits bitmaps up to 46,340 x 46,340
return getRowBytes() * getHeight();
}
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);
}
#Bitmap.cpp
static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) {
SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle)
return static_cast<jint>(bitmap->rowBytes());
}
#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 占用記憶體的計算方式為 b i t m a p I n R a m = b i t m a p W i d t h ∗ b i t m a p H e i g h t ∗ 4 b y t e s bitmapInRam = bitmapWidth*bitmapHeight *4 bytes bitmapInRam=bitmapWidth∗bitmapHeight∗4bytes
一張522*686的 PNG 圖檔,把它放到 drawable-xxhdpi 目錄下,在三星s6上加載,占用記憶體2547360B,就可以用這個方法擷取到。
然而公式計算出來1432368B
density影響記憶體占用
Bitmap占用空間的大小不止和圖檔的寬高有關,還與密度因子有關。
讀取的是 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) {
//密度等于TypedValue.DENSITY_NONE,那麼就沒有與資源相關的密度,它不應該被縮放
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
//密度等于這個值,那麼這個密度應該被視為系統的預設密度值
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;//預設密度160
} 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);
}
我們重點關注兩個值 inDensity 和 inTargetDensity,他們與BitmapFactory.cpp檔案裡面的 density 和 targetDensity相對應
inDensity 就是原始資源的 density,inTargetDensity 就是螢幕的 density。
接着,用到了 nativeDecodeStream 方法,其中最關鍵的 doDecode 函數的代碼:
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");
}
//這裡這個decodingBitmap就是解碼出來的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 其實是decodingBitmap的 densityDpi ,跟這張圖檔的放置的目錄有關(比如 hdpi 是240,xxhdpi 是480),
targetDensity 實際上是我們加載圖檔的目标 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
值還是不一樣
精度影響記憶體占用
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
Bitmap 在記憶體當中占用的大小的影響因素
- 色彩格式,如果是 ARGB8888 那麼就是一個像素4個位元組,如果是 RGB565 那就是2個位元組
- 原始檔案存放的資源目錄
- 目标螢幕的密度
如何優化
知道了原因,那麼據此即可優化記憶體使用。
詳情可以檢視優化Bitmap記憶體占用