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=bitmapWidthbitmapHeight4bytes

一张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内存占用

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值