浅析Bitmap占据内存大小
来源:互联网 发布:mysql开启远程连接 编辑:程序博客网 时间:2024/06/03 17:20
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
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 当中,这两个的关系是线性的:
这些内容每个人应该都知道,先放到这里,方便后面查表。
非压缩计算
如果图片不被压缩,按照常规计算内存大小方法为:
//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
吧,请教一下为何。
这些瞎猜只能暂时放着,等以后有机会再验证吧。。。。。。
- 浅析Bitmap占据内存大小
- 数据占据内存大小
- Android图片占据的内存大小分析
- bitmap占用内存大小
- bitmap占用内存大小总结
- Bitmap 不同类型占内存大小
- 计算bitmap占用内存大小
- struct与union内存大小浅析
- Bitmap占用内存大小的准确计算公式
- ImageView,Bitmap的浅析
- C++结构体占用内存大小浅析
- android 获取Bitmap位图所占用的内存大小
- 浅析Bitmap类的mBuffer
- 让空灵占据心灵
- div占据整个body
- 占据坏物理内存
- Bitmap
- bitmap
- 下拉框赋值
- python3.6+wampserver搭建简单CGI环境(Windows)
- JZOJ 5195. 【NOIP2017提高组模拟7.3】A
- 51Nod-1616-最小集合
- XML
- 浅析Bitmap占据内存大小
- 安卓自定义View进阶-Matrix原理
- If 判断、switch语句和三中循环语句
- varchar 和varchar2的区别 rowid 和rownub的区别
- Oracle数据库关闭归档功能及删除归档日志方法
- 使用mongodb处理上亿级别数据
- Python变量类型
- mac命令行使用笔记
- 赛下六首