版权声明:
本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有。
每周会统一更新到这里,如果喜欢,可关注公众号获取最新文章。
未经允许,不得转载。
一、前言
在一个 App 中,无可避免的会有一些 Bitmap 的资源,会被打包在 apk 中,随着 apk 发布出去。而当你在使用这些 Bitmap 的资源的时候,它到底需要占用多少内存空间?这是一个很实际的问题,把握不好就可能引发各种 OOM 的错误。
本文就来探讨一下,本地的 Bitmap 到底占用多少内存空间?
二、占用多少内存?
2.1 如何获取占用的内存空间?
既然需要说道一个 Bitmap 资源,加载到内存中所要占用的空间,那就需要有一个明确的获取方法,来确定的知道它到底占用了多少空间。而 Android 确实也为我们提供了类似的 API,那就是 Bitmap.getByteCount()
。
例如,现在项目内有一个 400 * 200 像素的图片,方在 drawable-xhdpi 目录下,在 Nexus 6 设备上,运行加载它。看它输出的尺寸。
看一下输出的结果:
I/cxmyDev: byteCound : 720000复制代码
可以看到,getByteCount()
是根据 getRowBytes() * getHeight()
计算出来的。getHeight()
方法它是 Bitmap 的高度,而 getRowBytes()
又是什么?
2.2 getRowBytes() 的计算依据
getRowBytes()
方法,最终调用的是一个 nativeRowBytes()
的方法,它是一个 native 的方法。
既然要查就查到底,看看 native 的代码是如何实现的(文内 native 的源码,都是基于 Android 5.1.1,文末会有在线查看地址,并且已经附带行号,方便查阅)。
先看看 Bitmap.cpp 的代码中 rowBytes()
是如何实现的。
这里阅读的是 Android 5.1.1 的源码,实际上从 Android 6 开始,会使用 LocalScopedBitmap 去操作,它其实也只是对 SkBitmap 做了一个封装而已。如下图所示,rowBytes() 是使用的 LocalScoopedBitmap 来操作的,有兴趣的可以继续看看它是如何实现的。
可以看到,最终使用的是 SkBitmap 去实现的。
在 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 目录。
上面是官方提供的一张比较经典的图,可以看到,不同的目录,代表不同的 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 即可。
看到 Nexus 5 输出的结果:
I/cxmyDev: density : 3.0
I/cxmyDev: densityDpi : 480复制代码
了解了设备的 density 和 densityDpi ,在继续看看加载 Bitmap 的过程,使用的是 BitmapFactory.decodeResource()
方法。
从源码上可以看出,它实际上是分两步完成的。
- 使用
openRawResource()
方法获取图片的原始流。 - 使用
decodeResourceStream()
方法,对数据流进行解码和适配。
对于一个文件流而言,在这里我们是不需要关心的。主要影响图片内存的是 decodeResourceStream()
方法中,对数据流进行解码和适配的时候,都做了哪些处理。
在这个方法中,会传递一个 Options 的对象,用于配置当前图片的解码和适配。
从代码中可以了解到,影响图片内存占比的因素有 inDensity 和 inTargetDensity 两个。
Options 中这两个值,都是可以设置的,如果不对其进行额外的操作,它们默认情况下,分别表示的含义:
- inDensity :图片存放的 Drawable 文件夹代表的 densityDpi 。
- inTargetDensity : 当前设备固有的 densityDpi 。
而使用他们的代码,都是在 native 中,继续追看 BitmapFactory.cpp 的源码(源码太多,只贴关键点)
可以看到,它实际上是会通过两个 density 计算出一个比例值 scale ,它会去对图片原始的像素进行 scale 表示的比例的缩放。
也就是说同一张图片,放在不同 drawable 文件夹下的图片,在不同的设备上,实际上加载出来的尺寸也是不同的。
那计算图片内存的公式,就应该调整为:
scale = targetDensity / inDensity
bitmapInRam = (bitmapWidth*scale) * (bitmapHeight*scale) * 4 bytes复制代码
再来使用新的公式,计算一下上面图片的尺寸:
400 * (480/320) * 200 *(480/320) * 4 = 720000复制代码
可以看到,最终得出的和我们程序中计算的值一致 了,所以这就是我们最终得到的计算图片在内存中,占比的公式了。
再改写上面的 Demo ,把细节点都输出出来。
看看我们关心的 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 中。
所以 getByteCount()
这个 Api 得到的尺寸,可能和我们前面使用公式计算的尺寸,略微有些偏差,这个值就是在小数点之间。
4、小结
好了,到这里就讲清楚了一个本地的 Bitmap ,加载到内存中,到底会占用多少内存。
决定 Bitmap 占用内存大小的因素,和图片文件在磁盘上占用的空间一点关系都没有,总结来说,有以下几点:
- 色彩格式:比如 ARGB_8888 、RGB_5555 这种,单位像素占的内存空间不同。
- 图片本身的像素尺寸。
- 图片文件存放的 Drawable 目录。xhdpi 和 xxhdpi 可是不一样的。
- 目标设备的 densityDpi 值。
最后附上Android 5.1.1 的相关源码,供大家参考
- Bitmap.cpp :
- SkBitmap.cpp:
- BitmapFactory.cpp:
扫码关注吧~