爆裂吧现实

不想死就早点睡

0%

解析Bitmap的density

Bitmap 是内存优化逃不了的一个东西,本文探讨下,Bitmap 中的 density 到底是什么东西,它是如何影响到内存的使用的

先看下 density 的文档注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* <p>Returns the density for this bitmap.</p>
*
* <p>The default density is the same density as the current display,
* unless the current application does not support different screen
* densities in which case it is
* {@link android.util.DisplayMetrics#DENSITY_DEFAULT}. Note that
* compatibility mode is determined by the application that was initially
* loaded into a process -- applications that share the same process should
* all have the same compatibility, or ensure they explicitly set the
* density of their bitmaps appropriately.</p>
*
* @return A scaling factor of the default density or {@link #DENSITY_NONE}
* if the scaling factor is unknown.
*
*/

简单来说 density 是用来绘制缩放用的,默认情况下的 density 就是屏幕的 density(resources.displayMetrics.densityDpi),假如我修改了一张 Bitmap 的 density,那么图片的显示应该会发生缩放,写个简单的 demo 验证下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.male_xhdpi)
Log.i("Bitmap", "display's density: ${resources.displayMetrics.densityDpi}")
Log.i("Bitmap", "source bitmap's density: ${bitmap.density}")
Log.i("Bitmap", "source bitmap width: ${bitmap.width}, height: ${bitmap.height}")
img_src.setImageBitmap(bitmap)

val smallBitmap = bitmap.copy(bitmap.config, bitmap.isMutable)
Log.i("Bitmap", "smallBitmap width: ${smallBitmap.width}, height: ${smallBitmap.height}")
smallBitmap.density = bitmap.density * 2
img_small.setImageBitmap(smallBitmap)

val bigBitmap = bitmap.copy(bitmap.config, bitmap.isMutable)
Log.i("Bitmap", "bigBitmap width: ${bigBitmap.width}, height: ${bigBitmap.height}")
bigBitmap.density = bitmap.density / 2
img_big.setImageBitmap(bigBitmap)

界面
Imgur

输出:

1
2
3
4
5
display's density: 420
source bitmap's density: 420
source bitmap width: 360, height: 360
smallBitmap width: 360, height: 360
bigBitmap width: 360, height: 360

从输出可以看出,Bitmap 的 density 只是会影响到显示而已,并不会影响到 Bitmap 本身的大小,所以这个属性不会影响到内存占用过多的问题

界面中可以看出,density 导致图像的缩小一倍和放大一倍

那么影响内存的是什么呢,我们知道把一张 xxhdpi 的图片放到 xhdpi 中是不行的,这样会导致图片扩大,这里的扩大是指图片本身内存占用的扩大,而不是显示上面的,跟踪下 BitmapFactory.decodeResource(resources, R.drawable.male_xhdpi) 的代码调用,跟踪到 decodeResourceStream() 函数的时候,发现了 density 的身影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
validate(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);
}

发现 Options 里面有两个属性 (inDensityinTargetDensity),inTargetDensity 被赋值成了当前设备的像素密度,那么 inDensity 被赋值成啥了呢?代码中看是赋值成了参数 value 的 density,回溯函数看看 value.density 是哪里来的

1
2
3
4
5
6
7
8
public static Bitmap decodeResource(Resources res, int id, Options opts) {
// ...
try {
final TypedValue value = new TypedValue();
is = res.openRawResource(id, value);
}
// ...
}

TypeValue 的值只有在这里有写入现象,猜测是根据 resource 的等级来赋值的,比如 xhdpi 就是 320dpi,xxhdpi 就是 480dpi(这些数值可以在官网查看),写段代码测试下

1
2
3
4
5
6
7
val xhdpiValue = TypedValue()
resources.openRawResource(R.drawable.male_xhdpi, xhdpiValue)
Log.i("Bitmap", "xhdpi's density: ${xhdpiValue.density}")

val xxhdpiValue = TypedValue()
resources.openRawResource(R.drawable.male_xxhdpi, xxhdpiValue)
Log.i("Bitmap", "xhdpi's density: ${xxhdpiValue.density}")

输出

1
2
xhdpi's density: 320
xxhdpi's density: 480

恩,看来这个推测很准确

既然知道了这两个数值是什么意义,那么继续跟踪代码,跟踪后发现最后调用的是一个 native 函数

1
2
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);

androidXRef 看下这部分的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is
, jbyteArray storage, jobject padding, jobject options) {

jobject bitmap = NULL;
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

if (stream.get()) {
std::unique_ptr<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
SkASSERT(bufferedStream.get() != NULL);
// 在这里进行解码
bitmap = doDecode(env, bufferedStream.release(), 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) {
// 这里通过 density 计算缩放数值
scale = (float) targetDensity / density;
}
}
// ...
// Scale is necessary due to density differences.
if (scale != 1.0f) {
willScale = true;
// 计算出缩放后的 width 和 height
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
// ...
if (willScale) {
// 计算需要缩放的比例
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
// ...
// 可以看出把解码的 Bitmap 通过 canvas 缩放后绘制到 outBitmap,用于返回
SkCanvas canvas(outputBitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}
}

忽略了多余的代码,分析部分看中文注释

可以在代码中看到,Options 的 inDensityinTargetDensity 是用作 Bitmap 的缩放用的,此缩放并不是视觉上的缩放,而是缩放了 Bitmap 的真正尺寸,那么这里就需要注意内存上的消耗了,不要把 xxhdpi 的图片放到 xhdpi 下面

对于上面的 inScreenDensity,在 decodeResource 的流程里面并没有发现它的赋值过程,那么它肯定是初始值 0,查看了下注释,他表示当前 display 设备的 density,如果 inScreenDensity == density 就不会对图片进行缩放

总结下过程:
通过 resource 获取 Bitmap 的时候,先根据资源文件获得 inDensityinTargetDensity 就是当前设备的 density,图片解码后在通过 canvas 缩放 inTargetDensity / inDensity 个倍数,就获得了缩放尺寸后的 Bitmap