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 当中,这两个的关系是线性的:
density | 0.75 | 1 | 1.5 | 2 | 3 | 3.5 | 4 |
---|
densityDpi | 120 | 160 | 240 | 320 | 480 | 560 | 640 |
DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi | xxxxhdpi |
这些内容每个人应该都知道,先放到这里,方便后面查表。
非压缩计算
如果图片不被压缩,按照常规计算内存大小方法为:
public final int getByteCount() {
return getRowBytes() * getHeight();
}
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)
size_t rowBytes() const { return fRowBytes; }
|
得到上述fRowBytes的大小会在SkBitmap.cpp文件里计算:(/external/skia/src/core/SkBitmap.cpp)
size_t SkBitmap::ComputeRowBytes(Config c, int width) {
return SkColorTypeMinRowBytes(SkBitmapConfigToColorType(c), width);
}
static inline size_t SkColorTypeMinRowBytes(SkColorType ct, int width) {
return width * SkColorTypeBytesPerPixel(ct);
}
static int SkColorTypeBytesPerPixel(SkColorType ct) {
static const uint8_t gSize[] = {
0,
1,
2,
2,
4,
4,
1,
};
...省略障眼法的宏...
return gSize[ct];
}
SkColorType SkBitmapConfigToColorType(SkBitmap::Config config) {
static const SkColorType gCT[] = {
kUnknown_SkColorType,
kAlpha_8_SkColorType,
kIndex_8_SkColorType,
kRGB_565_SkColorType,
kARGB_4444_SkColorType,
kN32_SkColorType,
};
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;
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);
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) {
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;
}
}
if (opts.inTargetDensity == 0 && res != null) {
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);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
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");
}
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());
SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
colorType, decodingBitmap.alphaType()));
if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
return nullObjectReturn("allocation failed for scaled bitmap");
}
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 {
ALPHA_8 (1),
RGB_565 (3),
ARGB_4444 (4),
ARGB_8888 (5);
inal int nativeInt;
}
|
其实 Java 层的枚举变量的 nativeInt 对应的就是 Skia 库当中枚举的索引值;而skia却支持这么多:
enum Config {
kNo_Config,
kA8_Config,
kIndex8_Config,
kRGB_565_Config,
kARGB_4444_Config,
kARGB_8888_Config,
};
|
上述枚举中第三个类型为索引图类型。索引位图,每个像素只占 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 = 2;
return android.os.Build.VERSION.SDK_INT == 19 ? createBitmap19(width, height,
nativeConfig, hasAlpha) : createBitmap(width, height, nativeConfig, hasAlpha);
}
|
将色彩格式改为索引色,然后重新编译运行。。。。。。然而弹幕压根没出来。。。。。等以后有机会问问ctiao
吧,请教一下为何。
这些瞎猜只能暂时放着,等以后有机会再验证吧。。。。。。