Android Bitmap 到底占了多少内存

283 阅读7分钟
原文链接: www.jianshu.com

前言

在Android的内存优化中,对Bitmap的优化绝对是主角,因为Bitmap对内存的影响很大,稍有不慎就很容易引起OOM的问题。不信的话就随我来看看Bitmap到底能吃掉多少内存。

预备知识

本篇文章不会讲到任何源码的东西,但还是需要有一定的预备知识的。

Bitmap的色彩模式,目前常见的有两种模式:

  1. Config.RGB_565:565分别对应着表示RGB所需要的位数,加起来是16位,也就是一个像素需要2个字节来表示。这种模式下不支持Alpha通道。
  2. Config.ARGB_8888:这是默认的选项,每个通道占8位,所以一个像素需要4个字节来表示。这种模式质量最高,占的内存也高。

我们在Android上经常使用的单位是dp,1dp等于多少像素其实是与设备的密度有关系的,比如说我们现在最常见的1080 * 1920分辨率的手机,它的屏幕密度是480,对应起来,1dp = 3px,对应的资源目录是drawable-xxhdpi

density 1 1.5 2 3 4
densityDpi 160 240 320 480 640
资源目录 mdpi hdpi xhdpi xxhdpi xxxhdpi

Bitmap 占了多少内存

这个问题换成以前,我可能就会直接回答,很简单啊。假设这张图片是ARGB_8888的,那这张图片占的内存就是 width * height * 4个字节。调用Bitmap.getByteCount()返回的也是这个计算结果。
后来因为工作关系,接触到Bitmap比较多,才发现这个回答其实只答对了一半,回答正确只是因为碰巧而已。Bitmap占用的内存还跟屏幕密度有关系。接下来就是动手的实验求真知的阶段了。

前提条件:图片大小:450 * 337, 手机是1080P的,对应xxhdpi目录, 颜色模式为ARGB_8888,按照上面的算法算的话结果应该是 450 * 337 * 4 = 606600
步骤:将图片分别放在drawable,drawable-xhdpi, drawable-xxhdpi, drawable-xxxhdpi,将加载出来的Bitmap大小以及占用内存打印出来。
结果:

资源目录 drawable drawable-xhdpi drawable-xxhdpi drawable-xxxhdpi
Bitmap大小 1350 * 1011 675 * 506 450 * 337 338 * 253
占用内存 5459400 1366200 606600 342056

发现没有,只有放在drawable-xxhdpi目录的图片结果才跟我们上面算的一样。放在其它目录的结果是Bitmap大小变了,导致占用内存也相应的变化了。
造成这样的结果的原因就是上面提到的屏幕密度了。当图片放在drawable-xhdpi目录下,但是需要显示在xxhdpi设备上时,这张图片会被认为是低密度设备需要的,现在要显示在高密度设备上,需要做一个放大,带来的结果就是图片变大了,占用内存也变大了。
那么需要放大多少呢?也是跟图片放置的目录和手机的密度有关系。还是以这个例子来说,需要放大的倍数是:480 / 320 = 1.5,即宽和高都放大1.5倍。再来手动计算一次好了:450 * 1.5 * 337 * 1.5 * 4 = 675 * 505.5 * 4 = 1364850,跟计算结果相差一丢丢,但已经很接近了。注意到计算的过程中有浮点数,而结果是整数,所以应该考虑下是不是精度问题导致的了。其实真正的计算结果是这样的:

width = (int) (405 * 1f / 320 * 480 + 0.5f) = 675
height = (int) (337 * 1f / 320 * 480 + 0.5f) = 506
byteCount = 675 * 506 * 4 = 1366200

不要问为什么是这样子的,因为源码里的计算方式就是这样子滴。

从这个例子中,我们也可以看得出如果图片资源放错目录,可能会带来什么样的后果。特别是我们可能很容易的就把图片放到drawable目录下,因为这个是默认的目录。但实际上它代表的是drawable-mdpi。试想一下,如果我们把图片都放在这个目录下,而手机是xxhdpi的,那么每张图片的占用将是原来的9倍啊同学们!!所以在开发过程中一定要注意把资源放置到正确的目录下。

上面说的是decodeResource的情况,而如果是decodeStream的话一般不会有上面的这种情况,所以计算方式就很直接很简单了。但是如果在解码时传入的options指定了inDensityinTargetDensity的话,那么情况又跟上面的例子类似了。

另外,图片的内存占用大小也受图片颜色模式的影响,如果我们把颜色模式设置为RGB_565,那直接就可以省下一半的内存了。对JPG格式的图片我们就可以考虑这样子做,因为它没有alpha通道。当然了,图片的质量也会下降一些,这个就需要去评估一下值不值得了。

到了这里,也算是解答完Bitmap占多少内存的问题了。不过这过程中又发现了一个有趣的问题。

getByteCount() & getAllocationByteCount()

在查看Bitmap的占用内存时,我发现了这两个很相似的api,于是就在打log的时候将这两个方法的结果都打了出来,结果发现都是一样的。但既然有两个api,就说明他们一定是有什么区别的,于是就查了一下资料,也在这里做一个补充说明吧。
通常情况下,这两个api是没有区别的,但如果你做了Bitmap复用,那他们就开始有区别了。在Android 3.0之后,Android支持了Bitmap复用,也就是说旧Bitmap的内存可以直接给新Bitmap用,不用再去申请内存了,前提条件是这两张Bitmap占用的大小一样大。到了Android 4.0之后,这一条件放宽了,只要旧Bitmap占用的内存大于新Bitmap所需要的内存,就可以直接复用了。还是举一个例子:先加载一张大一点的图片,然后用这张图片去给一张小一点的图片复用:

val largeOption = BitmapFactory.Options()
// 一定要加上这行代码,否则不生效
largeOption.inMutable = true
val largeBitmap = BitmapFactory.decodeResource(resources, R.drawable.large, largeOption)
Log.d(TAG, "bitmap is $largeBitmap, bitmap size is (${largeBitmap.width}, ${largeBitmap.height}),  byteCount = ${largeBitmap.byteCount}, allocationByte = ${largeBitmap.allocationByteCount}")

val smallOption = BitmapFactory.Options()
smallOption.inBitmap = largeBitmap
val smallBitmap = BitmapFactory.decodeResource(resources, R.drawable.small, smallOption)
Log.d(TAG, "bitmap is $smallBitmap, bitmap size is (${smallBitmap.width}, ${smallBitmap.height}), byteCount = ${smallBitmap.byteCount}, allocationByte = ${smallBitmap.allocationByteCount}")

结果是:

bitmap is android.graphics.Bitmap@cd9eadf, bitmap size is (600, 600), byteCount = 1440000, allocationByte = 1440000
bitmap is android.graphics.Bitmap@cd9eadf, bitmap size is (400, 250), byteCount = 400000, allocationByte = 1440000

可以看出,这两张Bitmap都是同一个对象来着,第一张Bitmap由于没有复用,所以byteCount == allocationByte。第二张Bitmap由于复用了第一张,byteCount表示当前Bitmap所占内存的大小,而allocationByte表示被复用Bitmap真实占用内存大小。所以如果还有新的Bitmap,只要它所需的内存小于allocationByteCount就可以了。再来实验一下:

// 这张Bitmap的大小介于largeBitmap和smallBitmap之间
// 选择复用smallBitmap
val normalOption = BitmapFactory.Options()
normalOption.inBitmap = smallBitmap
val normalBitmap = BitmapFactory.decodeResource(resources, R.drawable.normal, normalOption)
Log.d(TAG, "bitmap is $normalBitmap, bitmap size is (${normalBitmap.width}, ${normalBitmap.height}), byteCount = ${normalBitmap.byteCount}, allocationByte = ${normalBitmap.allocationByteCount}")

得到的结果是:

bitmap is android.graphics.Bitmap@cd9eadf, bitmap size is (450, 337), byteCount = 606600, allocationByte = 1440000

Bitmap还是原来的对象,复用也成功了。所以如果要使用Bitmap复用,需要用到的应该是getAllocationByteCount()方法去判断能否做复用。写到这里,突然想起了之前项目里用到的Bitmap复用,用的判断方法还是getByteCount()。虽然这样写也不会报错什么的,但是如果是 byteCount < 新Bitmap所需内存 < allocationByte这种情况的话,就会造成本可以复用的Bitmap却无法复用而需要去重新申请内存空间。不说了,等明年上班了赶紧改回来,这也算是写这篇文章的一个小收获了。

尾声

看了下上一篇的写作时间,才发现已经大半年没更新博客了,有点惭愧。这篇文章也算是为2018年划上一个句号吧,希望2019年能勤劳一些,多写写博客总结。