浅析Bitmap占据内存大小

xiaoxiao2021-02-28  23

April is your lie 首页 归档 分类 标签 关于 搜索

浅析Bitmap占据内存大小

2016-07-12 0 27 428

        Bitmap的使用是开发时绕不过的坑,使用时要处处留意内存问题,稍有不慎就会报OOM(out of memory)。所以这次我们就研究研究程序中Bitmap到底占据多少内存。

前奏

        比如我们使用一张图片,将其放入到工程目录中,想当然的会以为为这张图片建立的bitmap使用内存大小为:宽×高×像素大小。为了验证这个猜想,我在度娘上随便找了幅图:

素材

        规格是768×1152,大小为153.3KB,格式为支持ARGB四阶的32位色的JPG图片。         我们猜想,如果按照内存大小计算公式,所占内存应为: 768×1152×4=3538944,字节。因为JPG格式是有损压缩格式,所以存储大小比内存大小小多了。         然后将这张图片放到 res/drawable-xhdpi下,通过如下代码计算内存大小: float density = this.getResources().getDisplayMetrics().density; int dpi = this.getResources().getDisplayMetrics().densityDpi; Log.e(TAG, "density = " + density + "------" + "dpi = " + dpi); Bitmap b = BitmapFactory.decodeResource(getResources(), R.drawable.picture); int w = b.getWidth(); int h = b.getHeight(); int size = b.getByteCount(); int config = b.getConfig().ordinal(); Log.e(TAG, "w = " + w + ";" + "h = " + h + ";" + "size = " + size + ";" + "config = " + config);

       测试机器规格为:Google Nexus 5 - 5.1.0 - API 22 - 1080×1920(480dpi)。        打印log如下:

density = 3.0——dpi = 480 w = 1152;h = 1728;size = 7962624;config = 3

excuse me

       Why?How did you do it?这个不按套路出牌啊,宽高明显被拉伸了啊。。。。。。然后我又试了下将这张图片放到了 res/drawable-xxhdpi下,打印log如下:

density = 3.0——dpi = 480 w = 768;h = 1152;size = 3538944;config = 3

       这次倒是和理论计算的大小一样了,我们大概猜到了什么。。。。。接着我又把这张图片放到了assets目录下,然后修改了一下获取图片的代码,打印log如下:

density = 3.0——dpi = 480 w = 768;h = 1152;size = 3538944;config = 3

       这次也是和理论值一样的,因为放到assets目录下的图片是不会被压缩的。

       如果多试几次,把图片放入不同目录下再运行几遍,我们也能够总结出规律的。但这些都是现象,我们组的老大也曾经说过:开发人员不要轻易根据现象得出结论…….所以我们也要分析一下本质原因。

求证

       做适配的同学要经常和density、densityDpi搞好关系,简单来说,可以理解为 density 的数值是 1dp=density px;densityDpi 是屏幕每英寸对应多少个点(不是像素点),在 DisplayMetrics 当中,这两个的关系是线性的:

density0.7511.5233.54densityDpi120160240320480560640DpiFolderldpimdpihdpixhdpixxhdpixxxhdpixxxxhdpi

       这些内容每个人应该都知道,先放到这里,方便后面查表。

非压缩计算

       如果图片不被压缩,按照常规计算内存大小方法为:

//Bitmap的getByteCount方法 public final int getByteCount() { // int result permits bitmaps up to 46,340 x 46,340 return getRowBytes() * getHeight(); } //Bitmap的getRowBytes方法 public final int getRowBytes() { return nativeRowBytes(mNativeBitmap); } private static native int nativeRowBytes(long nativeBitmap);

       getHeight 就是图片的高度(单位:px),getRowBytes 从字面意思看应该是行字节大小。我们往下看,找找JNI实现,查看 frameworks/base/core/jni/android/graphics/Bitmap.cpp文件:

static jint Bitmap_rowBytes(JNIEnv* env, jobject, jlong bitmapHandle) { SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle); return static_cast<jint>(bitmap->rowBytes()); }

       (reinterpret_cast和static_cast是C++经常用到的用来处理无关类型之间转换的强制类型转换符,建议有时间可以研究研究,或者把C++回顾一下,毕竟挺重要的。这里先给个科普文章)        上一篇关于的弹幕文章提到过,java层的Bitmap对应native层是由skia图形引擎创建的SkBitmap,关于skia这玩意儿东西比较多,不是专业的一时半会儿也玩不转。所以我们还是简单看看,继续往下找SkBitmap:(/external/skia/include/core/SkBitmap.h)

/** Return the number of bytes between subsequent rows of the bitmap. */ size_t rowBytes() const { return fRowBytes; }

       得到上述fRowBytes的大小会在SkBitmap.cpp文件里计算:(/external/skia/src/core/SkBitmap.cpp)

//计算fRowBytes大小 size_t SkBitmap::ComputeRowBytes(Config c, int width) { return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);//SkColorTypeMinRowBytes是/SkImageInfo.h的方法;SkBitmapConfigToColorType是SkImagePriv.cpp的方法 } //SkImageInfo.h的SkColorTypeMinRowBytes方法 static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) { return width * SkColorTypeBytesPerPixel(ct); } //SkImageInfo.h的SkColorTypeBytesPerPixel方法 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 }; ...省略障眼法的宏... return gSize[ct]; } //SkBitmapConfigToColorType是SkImagePriv.cpp的方法 SkColorType SkBitmapConfigToColorType(SkBitmap::Config config) { static const SkColorType gCT[] = { kUnknown_SkColorType, // kNo_Config kAlpha_8_SkColorType, // kA8_Config kIndex_8_SkColorType, // kIndex8_Config kRGB_565_SkColorType, // kRGB_565_Config kARGB_4444_SkColorType, // kARGB_4444_Config kN32_SkColorType, // kARGB_8888_Config }; SkASSERT((unsigned)config < SK_ARRAY_COUNT(gCT)); return gCT[config]; }

       跟踪到这里,还记得我们上面大log的地方么。int config = b.getConfig().ordinal()返回的是3,那么在Bitmap.Config里面索引第4个枚举变量:

public enum Config { ALPHA_8 (1), RGB_565 (3), ARGB_4444 (4), ARGB_8888 (5);//索引第四个是这个 final int nativeInt; //从这个列表可以看出它与skia支持的图片格式一一对应,但是Android只支持上面4种 private static Config sConfigs[] = { null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888 }; Config(int ni) { this.nativeInt = ni; } static Config nativeToConfig(int ni) { return sConfigs[ni]; } }

       依照上面C++文件,我们发现 ARGB_8888(也就是我们最常用的 Bitmap 的格式)的一个像素占用 4byte,那么 rowBytes 实际上就是 4*width bytes。则理论上 ARGB_8888 的 Bitmap 占用内存的计算公式为:

bitmapInRam = bitmapWidth × bitmapHeight × 4 bytes

压缩计算

       如果我们不将图片放到assets目录下,内存大小计算方式就和上面完全不同了。我们读取的是 drawable 目录下面的图片,用的是 decodeResource 方法,该方法本质上就两步:

读取原始资源,这个调用了 Resource.openRawResource 方法,这个方法调用完成之后会对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息;调用 decodeResourceStream 对原始资源进行解码和适配。这个过程实际上就是原始资源的 density 到屏幕 density 的一个映射。        原始资源的 density 其实取决于资源存放的目录(比如 xxhdpi 对应的是480),而屏幕 density 的赋值,请看下面这段代码: public static Bitmap decodeResource(Resources res, int id) { return decodeResource(res, id, null); } public static Bitmap decodeResource(Resources res, int id, Options opts) { Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); is = res.openRawResource(id, value);//对 TypedValue 进行赋值,其中包含了原始资源的 density 等信息 bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { ...... } finally{ ...... } ...... return bm; } public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) {//opt为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;//这里density的值如果对应资源目录为xhdpi的话,就是320 } } if (opts.inTargetDensity == 0 && res != null) { //请注意,inTargetDensity就是当前的显示密度,比如Google Nexus 5就是480 opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); } public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) { ...... bm = decodeStreamInternal(is, outPadding, opts); ...... return bm; } private static Bitmap decodeStreamInternal(InputStream is, Rect outPadding, Options opts) { ...... return nativeDecodeStream(is, tempStorage, outPadding, opts); } private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage, Rect padding, Options opts);

       我们看到 opts 这个值被初始化,而它的构造居然如此简单:

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

       所以我们就很容易的看到,Option.inScreenDensity 这个值没有被初始化,而实际上后面我们也会看到这个值根本不会用到;我们最应该关心的是什么呢?是 inDensity 和 inTargetDensity,这两个值与下面 cpp 文件里面的 density 和 targetDensity 相对应——重复一下,inDensity 就是原始资源的 density,inTargetDensity 就是屏幕的 density。        紧接着,用到了 nativeDecodeStream 方法:

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) { jobject bitmap = NULL; ...... bitmap = doDecode(env, bufferedStream, padding, options); return bitmap; } 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);//对应xhdpi的时候,是320 const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//Google Nexus 5为480 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"); } //这里这个deodingBitmap就是解码出来的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 和 targetDensity,前者是 decodingBitmap 的 density,这个值跟这张图片的放置的目录有关(比如 xhdpi 是320,xxhdpi 是480),这部分代码我跟了一下,太长了,就不列出来了;targetDensity 实际上是我们加载图片的目标 density,这个值的来源我们已经在前面给出了,就是 DisplayMetrics 的 densityDpi,如果是Google Nexus 5那么这个数值就是480。sx 和sy 实际上是约等于 scale 的,因为 scaledWidth 和 scaledHeight 是由 width 和 height 乘以 scale 得到的。我们看到 Canvas 放大了 scale 倍,然后又把读到内存的这张 bitmap 画上去,相当于把这张 bitmap 放大了 scale 倍。

       然后我们再次验证上面打log的地方,win + r ,输入calc呼出计算器。这里千万不要忘了了精度:

float scale = 480/320f = 1.5 int scaledWidth = int(768 * 1.5 + 0.5f) = 1152 int scaledHeight = int(1152 * 1.5 + 0.5f) = 1728

size = 1152 1728 4 = 7962624

       果然和上面log打印的一模一样!因此我们可以得出结论。Bitmap在内存中大小取决于:

色彩格式,前面我们已经提到,如果是 ARG_B8888 那么就是一个像素4个字节,如果是 RGB_565 那就是2个字节原始文件存放的资源目录(是 hdpi 还是 xxhdpi 等等)目标屏幕的密度(所以同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)

       内存大小计算公式大概为(压缩计算情况下)(已忽略精度):

内存大小 = (设备屏幕dpi / 资源所在目录dpi)^ 2 × 图片原始宽 × 图片原始高 × 像素大小

瞎猜

       上面分析Bitmap.Config时发现Android官方并不完全支持skia图形引擎的所有像素格式,供java层设置的Config只有这么4个:

public enum Config { // these native values must match up with the enum in SkBitmap.h ALPHA_8 (1), RGB_565 (3), ARGB_4444 (4), ARGB_8888 (5); inal int nativeInt; }

       其实 Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值;而skia却支持这么多:

//Skbitmap.h文件 enum Config { kNo_Config, //!< bitmap has not been configured kA8_Config, //!< 8-bits per pixel, with only alpha specified (0 is transparent, 0xFF is opaque) kIndex8_Config, //!< 8-bits per pixel, using SkColorTable to specify the colors kRGB_565_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing) kARGB_4444_Config, //!< 16-bits per pixel, (see SkColorPriv.h for packing) kARGB_8888_Config, //!< 32-bits per pixel, (see SkColorPriv.h for packing) };

       上述枚举中第三个类型为索引图类型。索引位图,每个像素只占 1 Byte,不仅支持 RGB,还支持 alpha。微软画图工具应该都玩过吧(win + r,输入mspaint),里面的调色板就是索引色盘。        而Android其他的config类型一个像素点占的字节比这个大多了,所以我们有时候能不能也用索引色去悄悄替换原来格式呢?        我的猜想是,反射构造一个Bitmap.Config枚举对象,然后反射设置nativeInt字段的值为2,猜想代码如下:

Options op = new Options(); op.inPreferredConfig = ...反射构建Bitmap.Config相关内容... BitmapFactory.decodeResource(getResources(), R.drawable.picture, op);

       不过我没有实践过,也是瞎猜的,不知道能不能行的通。。。。。。

       但是我对上一篇文章种调skia生成弹幕bitmap处的代码做了修改,修改了DanmakuFlameMaster工程里的NativeBitmapFactory.java文件:

private static Bitmap createNativeBitmap(int width, int height, Config config, boolean hasAlpha) { // int nativeConfig = getNativeConfig(config); int nativeConfig = 2;//直接改为索引色 return android.os.Build.VERSION.SDK_INT == 19 ? createBitmap19(width, height, nativeConfig, hasAlpha) : createBitmap(width, height, nativeConfig, hasAlpha); }

       将色彩格式改为索引色,然后重新编译运行。。。。。。然而弹幕压根没出来。。。。。等以后有机会问问ctiao吧,请教一下为何。        这些瞎猜只能暂时放着,等以后有机会再验证吧。。。。。。

转载请注明原文地址: https://www.6miu.com/read-1000279.html

最新回复(0)