Bitmap 比你想的更费内存 | 吊打 OOM

4,920 阅读6分钟

版权声明:

本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有。

每周会统一更新到这里,如果喜欢,可关注公众号获取最新文章。

未经允许,不得转载。

一、前言

在一个 App 中,无可避免的会有一些 Bitmap 的资源,会被打包在 apk 中,随着 apk 发布出去。而当你在使用这些 Bitmap 的资源的时候,它到底需要占用多少内存空间?这是一个很实际的问题,把握不好就可能引发各种 OOM 的错误。

本文就来探讨一下,本地的 Bitmap 到底占用多少内存空间?

二、占用多少内存?

2.1 如何获取占用的内存空间?

既然需要说道一个 Bitmap 资源,加载到内存中所要占用的空间,那就需要有一个明确的获取方法,来确定的知道它到底占用了多少空间。而 Android 确实也为我们提供了类似的 API,那就是 Bitmap.getByteCount()

/bitm-bytecount.png
/bitm-bytecount.png

例如,现在项目内有一个 400 * 200 像素的图片,方在 drawable-xhdpi 目录下,在 Nexus 6 设备上,运行加载它。看它输出的尺寸。

/bitm-getbitmap.png
/bitm-getbitmap.png

看一下输出的结果:

I/cxmyDev: byteCound : 720000

可以看到,getByteCount() 是根据 getRowBytes() * getHeight() 计算出来的。getHeight() 方法它是 Bitmap 的高度,而 getRowBytes() 又是什么?

2.2 getRowBytes() 的计算依据

/bitm-getrowbyte.png
/bitm-getrowbyte.png

getRowBytes() 方法,最终调用的是一个 nativeRowBytes() 的方法,它是一个 native 的方法。

既然要查就查到底,看看 native 的代码是如何实现的(文内 native 的源码,都是基于 Android 5.1.1,文末会有在线查看地址,并且已经附带行号,方便查阅)。

先看看 Bitmap.cpp 的代码中 rowBytes() 是如何实现的。

/bitm-cbitroumap5.png
/bitm-cbitroumap5.png

这里阅读的是 Android 5.1.1 的源码,实际上从 Android 6 开始,会使用 LocalScopedBitmap 去操作,它其实也只是对 SkBitmap 做了一个封装而已。如下图所示,rowBytes() 是使用的 LocalScoopedBitmap 来操作的,有兴趣的可以继续看看它是如何实现的。

/bitm-cbitroumap.png
/bitm-cbitroumap.png

可以看到,最终使用的是 SkBitmap 去实现的。

/bitm-skbmethod.png
/bitm-skbmethod.png

SkBitmap.cpp 里就可以确认 ,色彩度为 ARGB_8888 图片,每像素会占用 4 bytes 的大小。

看这个样子,结合前面提到的 Bitmap.getByteCount() 的计算公式就是:

bitmapInRam = bitmapWidth * 4 bytes * bitmapHeight

但是如果依据这样的公式计算一个结果,你会发现获得的值会比真实的值差了很多。

前面 Demo 中的图片,加载到内存中,占用的内存是:720000 。但是用我们这里得到的计算方式,计算的结果是。

400 * 200 * 4 = 320000

那么,问题出在哪里?

2.3 density 影响 Bitmap 内存

2.1 中的 Demo ,明确指出了需要图片存放的 Drawable 目录,以及使用的设备,其实它们都是有关系的,不是无关系的路人甲。

关于图片而言,放在不同的 Drawable 目录下,对应的不同 density 的设备。density 是设备的固有参数,伴随着 density 的,还有 densityDpi,它也是与设备相关的,表示屏幕每英寸对应多少个点(非像素点)。

它们之间的关系,可以直接查阅官方文档,这里就不赘述了。

developer.android.com/guide/pract…

这里说到的 density ,其实就是代表不同的 drawable-xxx 目录。

/bitm-screen.png
/bitm-screen.png

上面是官方提供的一张比较经典的图,可以看到,不同的目录,代表不同的 density ,例如 xhdpi 代表的 density 就是 2。而这里的 density 对 densityDip 的基准是 160 ,也就是说,mdpi 对应的 densityDpi 是 160 ,xhdpi 对应的 densityDpi 是 320。

它们的关系如下表:

density 1 1.5 2 3 3.5 4
densityDpi 160 240 320 480 560 640

density 和 densityDpi 在 Android 中,都有标准的 API 可以拿到,利用 DisplayMetrics 即可。

/bitm-densityapi.png
/bitm-densityapi.png

看到 Nexus 5 输出的结果:

I/cxmyDev: density : 3.0
I/cxmyDev: densityDpi : 480

了解了设备的 densitydensityDpi ,在继续看看加载 Bitmap 的过程,使用的是 BitmapFactory.decodeResource() 方法。

/bitm-getbitmapFra.png
/bitm-getbitmapFra.png

从源码上可以看出,它实际上是分两步完成的。

  1. 使用 openRawResource() 方法获取图片的原始流。
  2. 使用 decodeResourceStream() 方法,对数据流进行解码和适配。

对于一个文件流而言,在这里我们是不需要关心的。主要影响图片内存的是 decodeResourceStream() 方法中,对数据流进行解码和适配的时候,都做了哪些处理。

bitm-stream
bitm-stream

在这个方法中,会传递一个 Options 的对象,用于配置当前图片的解码和适配。

从代码中可以了解到,影响图片内存占比的因素有 inDensityinTargetDensity 两个。

Options 中这两个值,都是可以设置的,如果不对其进行额外的操作,它们默认情况下,分别表示的含义:

  • inDensity :图片存放的 Drawable 文件夹代表的 densityDpi 。
  • inTargetDensity : 当前设备固有的 densityDpi 。

而使用他们的代码,都是在 native 中,继续追看 BitmapFactory.cpp 的源码(源码太多,只贴关键点)

/bitm-scale.png
/bitm-scale.png

可以看到,它实际上是会通过两个 density 计算出一个比例值 scale ,它会去对图片原始的像素进行 scale 表示的比例的缩放。

也就是说同一张图片,放在不同 drawable 文件夹下的图片,在不同的设备上,实际上加载出来的尺寸也是不同的。

那计算图片内存的公式,就应该调整为:

scale = targetDensity / inDensity
bitmapInRam = (bitmapWidth*scale) * (bitmapHeight*scale) * 4 bytes

再来使用新的公式,计算一下上面图片的尺寸:

400 * (480/320) * 200 *(480/320) * 4 = 720000

可以看到,最终得出的和我们程序中计算的值一致 了,所以这就是我们最终得到的计算图片在内存中,占比的公式了。

再改写上面的 Demo ,把细节点都输出出来。

/bitm-code1.png
/bitm-code1.png

看看我们关心的 Log 输出:

I/cxmyDev: byteCound : 720000
I/cxmyDev: rowBytes : 2400
I/cxmyDev: height : 300
I/cxmyDev: width : 600
I/cxmyDev: density : 3.0
I/cxmyDev: densityDpi : 480

3.4 查缺补漏

前面举的例子中,图片尺寸和设备的 densityDpi 都是很规整的。但是不排除有一些比较不标准的设备,加载的图片使用上面的计算公式,依然对不上。

这个问题,还是需要在源码中找答案,对于不那么标准的 densityDpi 的设备而言,根据这个 scale 计算出来的尺寸,可能是一个 float 值,也就是存在小数的情况,而图片的尺寸,都是以 int 类型为单位。所以 Android 为了规避这样的问题,做了个容差值(0.5),去转换成 int 类型。

代码依然在 BitmapFactory..cpp 中。

/bitm-scalesize.png
/bitm-scalesize.png

所以 getByteCount() 这个 Api 得到的尺寸,可能和我们前面使用公式计算的尺寸,略微有些偏差,这个值就是在小数点之间。

4、小结

好了,到这里就讲清楚了一个本地的 Bitmap ,加载到内存中,到底会占用多少内存。

决定 Bitmap 占用内存大小的因素,和图片文件在磁盘上占用的空间一点关系都没有,总结来说,有以下几点:

  • 色彩格式:比如 ARGB_8888 、RGB_5555 这种,单位像素占的内存空间不同。
  • 图片本身的像素尺寸。
  • 图片文件存放的 Drawable 目录。xhdpi 和 xxhdpi 可是不一样的。
  • 目标设备的 densityDpi 值。

最后附上Android 5.1.1 的相关源码,供大家参考

  • Bitmap.cpp :

androidxref.com/5.1.1_r6/xr…

  • SkBitmap.cpp:

androidxref.com/5.1.1_r6/xr…

  • BitmapFactory.cpp:

androidxref.com/5.1.1_r6/xr…

公众号二维码.jpg
公众号二维码.jpg

扫码关注吧~