Bitmap内存分析

注意: 本文大部分数据是在SDK25, cpu架构为armeabi v7a, Android Studio 3.4上测试得出, 不同的系统版本和硬件可能有差异.

Bitmap内存计算方法

Bitmap在Android中是一个普通Java类, 关于Java类在内存中所占的大小可以参考Shallow Size和Retained Size详解.

在不同的Android版本中, Bitmap的定义有细微差别, 不过大致由两部分组成, 辅助变量Byte数组.

  1. Byte数组就是指保存所有像素的颜色信息的数组, 是Bitmap的核心信息, 一般也是内存大头.

  2. 辅助变量就是指Bitmap类内定义的除了Byte数组外的其他变量, 包括了图像的宽高等信息, 不同的SDK版本可能有所不同.

apk运行时, 所有Bitmap实例的辅助变量的内存的Shallow Size是相同的.

在SDK25中实测, 辅助变量的Shallow Size43Byte

本文主要关注Byte数组, 它的计算公式为

Byte数组大小 = 图像宽 * 图像高 * 单个像素所占字节

图像宽高可以直接通过Bitmap属性读取到. 所以

计算Bitmap内存的关键在于, 它用多少个字节表示单个像素

注意

图片文件的宽高不一定等于Bitmap宽高, 因为加载非屏幕对应dpidrawable资源文件时, 加载时会对图像进行缩放.

像素配置: ARGB_8888, RGB_565, ALPHA_8

我们通过BitmapFactory创建Bitmap时, 可以传BitmapFactory.Options控制加载配置, 可以通过inPreferredConfig参数来配置一个Bitmap.Config来指定加载配置.

一般常用的配置有ARGB_8888, RGB_565, ALPHA_8, 用来描述如何保存像素

  1. ARGB_8888表示一个像素有4个通道, 每个通道8bit, 共32bit, 所以单个像素占4byte
  2. RGB_565表示一个像素有3个通道, 没有透明通道, 其中R通道5bit, G通道6bit, B通道5bit, 共16bit, 所以单个像素占2byte
  3. ALPHA_8表示像素只有透明通道, 占1byte, 一般代码创建Bitmap用到, 用来创建透明遮罩用来进行图像计算.

但是注意, 该配置只是一个倾向, 如果需要加载的图像不能以该配置加载, 那么就会由系统自行选择合适的配置来加载.

例如, 带透明像素的图片不能以RBG_565的方式加载, 即使配置成RGB_565, 最终还是会以ARGB_8888来加载, 单个像素还是占4byte.

图片格式和像素配置的关系

以下结论都是在SDK25中实验得出

  1. PNG-32格式只能使用ARGB_8888配置加载, 设置RGB_565ALPHA_8无效
  2. PNG-24和PNG-8格式可以使用ARGB_8888RGB_565两种配置加载
  3. JPG/JPEG格式可以使用ARGB_8888RGB_565两种配置加载, 设置ALPHA_8无效
  4. WEBP格式, 当图中不包含透明像素时, 可以使用ARGB_8888RGB_565配置加载, 当图中包含透明像素时, 则只能使用ARGB_8888配置加载, 设置ALPHA_8总是无效的

在Android Studio打开格式为PNG, JPG和WEBP的图片时, 可以看到右上角有提示(如下图)该图标的像素深度, 常见的有32bit, 24bit和8bit, 可以根据这个值判断图片能否使用RGB_565加载. 像素深度为32bit的图片只能通过ARGB_8888加载

Android Studio图像预览, 像素深度提示

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#inPreferredConfignull, 来触发底层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数组, 规律如下

  1. 如果是ARGB_8888, 4个一组, 顺序为R-G-B-A

  2. 如果是RGB_565, 2个一组, 低位在前, 高位在后, (可能和CPU架构有关), 例如

    从profiler中观察到  98, 8
    实际应为
    8 98 = 0000 1000 0110 0010 -> R=00010, G=000011, B=00010
    
  3. profiler中的十进制负数需要转成二进制补码才是正确的颜色值, 例如

    ARGB_8888中观察到R=-16
    -16的机器码 -> 1001 0000
    1001 0000反码 -> 1110 1111
    1001 0000补码 = 反码 + 1 -> 1111 0000
    所以实际色值
    R=1111 0000=0xF0=240
    

你可能感兴趣的:(Bitmap内存分析)