注意: 本文大部分数据是在SDK25, cpu架构为armeabi v7a, Android Studio 3.4上测试得出, 不同的系统版本和硬件可能有差异.
Bitmap内存计算方法
Bitmap
在Android中是一个普通Java类, 关于Java类在内存中所占的大小可以参考Shallow Size和Retained Size详解.
在不同的Android版本中, Bitmap
的定义有细微差别, 不过大致由两部分组成, 辅助变量和Byte数组.
Byte数组就是指保存所有像素的颜色信息的数组, 是
Bitmap
的核心信息, 一般也是内存大头.辅助变量就是指
Bitmap
类内定义的除了Byte数组外的其他变量, 包括了图像的宽高等信息, 不同的SDK版本可能有所不同.
apk运行时, 所有Bitmap
实例的辅助变量的内存的Shallow Size
是相同的.
在SDK25中实测, 辅助变量的
Shallow Size
是43Byte
本文主要关注Byte数组, 它的计算公式为
Byte数组大小 = 图像宽 * 图像高 * 单个像素所占字节
图像宽高可以直接通过Bitmap
属性读取到. 所以
计算
Bitmap
内存的关键在于, 它用多少个字节表示单个像素
注意
图片文件的宽高不一定等于Bitmap
宽高, 因为加载非屏幕对应dpi
的drawable
资源文件时, 加载时会对图像进行缩放.
像素配置: ARGB_8888, RGB_565, ALPHA_8
我们通过BitmapFactory
创建Bitmap
时, 可以传BitmapFactory.Options
控制加载配置, 可以通过inPreferredConfig
参数来配置一个Bitmap.Config
来指定加载配置.
一般常用的配置有ARGB_8888
, RGB_565
, ALPHA_8
, 用来描述如何保存像素
-
ARGB_8888
表示一个像素有4个通道, 每个通道8bit, 共32bit, 所以单个像素占4byte -
RGB_565
表示一个像素有3个通道, 没有透明通道, 其中R通道5bit, G通道6bit, B通道5bit, 共16bit, 所以单个像素占2byte -
ALPHA_8
表示像素只有透明通道, 占1byte, 一般代码创建Bitmap
用到, 用来创建透明遮罩用来进行图像计算.
但是注意, 该配置只是一个倾向, 如果需要加载的图像不能以该配置加载, 那么就会由系统自行选择合适的配置来加载.
例如, 带透明像素的图片不能以RBG_565
的方式加载, 即使配置成RGB_565
, 最终还是会以ARGB_8888
来加载, 单个像素还是占4byte.
图片格式和像素配置的关系
以下结论都是在SDK25中实验得出
- PNG-32格式只能使用
ARGB_8888
配置加载, 设置RGB_565
和ALPHA_8
无效 - PNG-24和PNG-8格式可以使用
ARGB_8888
和RGB_565
两种配置加载 - JPG/JPEG格式可以使用
ARGB_8888
和RGB_565
两种配置加载, 设置ALPHA_8
无效 - WEBP格式, 当图中不包含透明像素时, 可以使用
ARGB_8888
和RGB_565
配置加载, 当图中包含透明像素时, 则只能使用ARGB_8888
配置加载, 设置ALPHA_8
总是无效的
在Android Studio打开格式为PNG, JPG和WEBP的图片时, 可以看到右上角有提示(如下图)该图标的像素深度, 常见的有32bit, 24bit和8bit, 可以根据这个值判断图片能否使用
RGB_565
加载. 像素深度为32bit的图片只能通过ARGB_8888
加载
RGB_565的效果
以下结论都是在SDK25中实验得出
因为使用RGB_565
时, 只能使用5bit表示红色通道和蓝色通道, 用6bit表示绿色通道, 所以颜色会和实际值有差别
经试验得出: 加载进内存时会移除低位, 显示时会在低位用1或0补全8位
我并没有看补全位数的源码, 根据现象推测是根据颜色值的大小来补位, 例如
图像实际颜色是R=240, G=8, B=30, #FF0081E
, 通过RGB_565
加载后, 内存中实际的颜色值是R=247, G=8,B=24, #F70818
, 其中红色通道进行了1补位, 绿色通道进行了0补位, 蓝色通道进行了0补位, 拿蓝色通过举例
// 蓝色通道原始颜色
30=0x1E=0001 1110
// 通过RGB_565加载时需要移除3位低位
0001 1110 -> 0001 1xxx
// 显示时使用0补位
0001 1xxx -> 0001 1000=0x18=24
黑科技: 索引位图(PNG8)
PNG格式
参考PNG-8、24、32区别介绍
PNG实际上是存在多种格式的, 而在Android中, 普通场景下说的PNG是指PNG32, 即单个像素是由4通道, 每通道8bit表示的, 所以每个像素占32位.
另外在官方文档资源类型-位图介绍中有提到, aapt
工具在打包时可以自动转成PNG-8.
注:在构建过程中,可通过
aapt
工具自动优化位图文件,对图像进行无损压缩。例如,不需要超过 256 色的真彩色 PNG 可通过调色板转换为 8 位 PNG。这样产生的图像质量相同,但所需内存更少。因此请注意,此目录中的图像二进制文件在构建时可能会发生变化。如果您打算以比特流的形式读取图像,进而将其转换为位图,请改为将图像放在res/raw/
文件夹中,避免系统对其进行优化。
索引位图内存
本节内容参考Android 内存优化之索引颜色位图(上)
具体的原理看上述链接即可, 核心原理是
通过设置
BitmapFactory.Option#inPreferredConfig
为null
, 来触发底层Skia加载索引位图的逻辑, 索引位图的特点是有一个色盘区域储存图像中使用的颜色, 而图像中每个像素用1byte来表示色盘中的颜色下标, 因此内存仅占ARGB_8888
的1/4, 可以大大降低Bitmap
的内存占用.
注意
虽然索引位图可以降低内存, 但是对图片本身的限制较多, 因为每个像素只占1byte, 也就是只能表示0-255, 所以索引位图最多只能包含256种颜色, 所以只适用于色彩较少的图像, 尤其不适用于有渐变颜色的图像, 另外通过实测, 还有需要注意
PNG-8的图可以直接放在
drawable
目录中, 但是通过普通资源id设置或者xml显示时, 还是会加载成ARGB_8888
, 需要手动通过BitmapFactory
加载才能优化内存.
Android Studio profiler内存工具观察色值
可以通过Android Studio的profiler抓内存看Bitmap
实例对应的byte数组, 规律如下
如果是
ARGB_8888
, 4个一组, 顺序为R-G-B-A-
如果是
RGB_565
, 2个一组, 低位在前, 高位在后, (可能和CPU架构有关), 例如从profiler中观察到 98, 8 实际应为 8 98 = 0000 1000 0110 0010 -> R=00010, G=000011, B=00010
-
profiler中的十进制负数需要转成二进制补码才是正确的颜色值, 例如
ARGB_8888中观察到R=-16 -16的机器码 -> 1001 0000 1001 0000反码 -> 1110 1111 1001 0000补码 = 反码 + 1 -> 1111 0000 所以实际色值 R=1111 0000=0xF0=240