Bitmap 是内存优化逃不了的一个东西,本文探讨下,Bitmap 中的 density 到底是什么东西,它是如何影响到内存的使用的
先看下 density 的文档注释
简单来说 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)
界面
输出:
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 里面有两个属性 (inDensity
,inTargetDensity
),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) { scale = (float ) targetDensity / density; } } if (scale != 1.0f ) { willScale = true ; 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()); SkCanvas canvas (outputBitmap) ; canvas.scale(sx, sy); canvas.drawBitmap(decodingBitmap, 0.0f , 0.0f , &paint); } }
忽略了多余的代码,分析部分看中文注释
可以在代码中看到,Options 的 inDensity
和 inTargetDensity
是用作 Bitmap 的缩放用的,此缩放并不是视觉上的缩放,而是缩放了 Bitmap 的真正尺寸,那么这里就需要注意内存上的消耗了,不要把 xxhdpi 的图片放到 xhdpi 下面
对于上面的 inScreenDensity
,在 decodeResource
的流程里面并没有发现它的赋值过程,那么它肯定是初始值 0,查看了下注释,他表示当前 display 设备的 density,如果 inScreenDensity == density
就不会对图片进行缩放
总结下过程: 通过 resource 获取 Bitmap 的时候,先根据资源文件获得 inDensity
,inTargetDensity
就是当前设备的 density,图片解码后在通过 canvas 缩放 inTargetDensity / inDensity
个倍数,就获得了缩放尺寸后的 Bitmap