天天看點

Android中一張圖檔需要占用多少記憶體

前置概念-螢幕密度

搞清楚 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記憶體占用