2. 图片内存模型:Bitmap 在内存中的占用计算方式

聊图片加载,第一个绕不开的问题就是:一张图片到底占多少内存?

我见过不少开发同学,图片加载 OOM 了,第一反应是「图片太大了,压缩一下」。但压缩的是文件体积,不是内存占用。这两者完全是两码事。文件体积是磁盘上的事,内存占用是解码后的事。

说白了,一张 1920×1080 的 JPEG,文件可能只有 200KB。但一旦解码成 Bitmap,它占的内存是固定的——跟文件大小没关系。

2.1 核心公式:宽 × 高 × 每像素字节数

Bitmap 的内存占用,就这一个公式:

内存占用 = 图片宽度(px) × 图片高度(px) × 每像素字节数(BPP)

举个例子:

  • 一张 1080×1920 的图片
  • 使用 ARGB_8888 格式(每像素 4 字节)
  • 内存 = 1080 × 1920 × 4 = 8,294,400 字节 ≈ 7.9 MB

嗯,一张图就快 8MB 了。如果你的列表页同时加载 10 张,那就是 80MB。在低端机上,这已经接近极限了。

关键认知:图片的内存占用只取决于 像素尺寸 × 色彩格式,与文件格式(JPEG/PNG/WebP)无关。文件压缩只影响磁盘和网络传输,不影响解码后的内存。

2.2 两种主流格式:ARGB_8888 vs RGB_565

Android 中 Bitmap 的 Config 枚举定义了像素的存储方式。最常用的两个:

Config 每像素字节 颜色通道 透明度 典型场景
ARGB_8888 4 字节 A(8bit) + R(8bit) + G(8bit) + B(8bit) 支持(256 级) 照片、渐变、需要透明度
RGB_565 2 字节 R(5bit) + G(6bit) + B(5bit) 不支持 无透明要求的图片、游戏素材

看到区别了吗?RGB_565 的内存只有 ARGB_8888 的一半。

同样一张 1080×1920 的图:

  • ARGB_8888:7.9 MB
  • RGB_565:3.95 MB

省了一半。代价是什么?颜色精度下降。RGB_565 只有 5 位红色、6 位绿色、5 位蓝色,总共 65536 种颜色。而 ARGB_8888 有 1600 万色以上。对于纯色块、UI 图标、游戏精灵来说,肉眼几乎看不出区别。但如果是照片,尤其是天空、皮肤这类渐变区域,RGB_565 会出现明显的色带。

我的建议:如果你确定图片不需要透明度,且对色彩精度要求不高(比如用户头像的缩略图、列表中的小图),可以考虑使用 RGB_565。我在做一款社交应用时,把列表中的头像从 ARGB_8888 切到 RGB_565,内存直接降了一半,用户完全没察觉差异。

2.3 其他格式一览

除了上面两个,Android 还支持其他格式,但用得少:

Config 每像素字节 说明
ALPHA_8 1 字节 只有透明度通道,用于遮罩
ARGB_4444 2 字节 已废弃,质量差,别用
RGBA_F16 8 字节 HDR 图片,每通道 16 位浮点
HARDWARE 视情况 硬件加速专用,不可直接读取像素

避坑:ARGB_4444 在 API 13 就已经被标记为废弃。我曾经在一个老项目里看到有人用这个格式来省内存,结果图片全是噪点。千万别用。

2.4 内存模型的核心逻辑

为了让你更直观地理解,我画了一张图:

Bitmap 内存模型与格式对比 Bitmap 内存模型:从像素到字节 像素矩阵 宽 × 高 例如:1080 × 1920 每个像素由 N 字节表示 × 每像素字节数 ARGB_8888 → 4 字节 RGB_565 → 2 字节 ALPHA_8 → 1 字节 RGBA_F16 → 8 字节 = 内存占用 宽 × 高 × BPP 1080×1920×4 = 7.9 MB vs 1080×1920×2 = 3.95 MB 💡 关键结论 • 内存占用 = 像素总数 × 每像素字节数,与文件格式(JPEG/PNG)无关 • ARGB_8888 是默认格式,支持透明度,但内存是 RGB_565 的两倍 • RGB_565 省一半内存,但无透明度、颜色精度低,适合非照片类图片 • 选择格式时,要在「内存」和「画质」之间做权衡

2.5 实际项目中的选择策略

我在项目中总结了一套简单的选择规则:

  1. 默认用 ARGB_8888——兼容性最好,质量最高。除非你明确知道不需要透明度。
  2. 列表中的小图、头像、图标——可以考虑 RGB_565。尤其是缩略图,人眼对细节不敏感。
  3. 游戏中的精灵图、UI 九宫格——RGB_565 是首选,省内存且不影响视觉效果。
  4. 照片、大图、背景图——坚持用 ARGB_8888。RGB_565 的色带在渐变区域非常明显。
  5. 遮罩、形状裁剪——用 ALPHA_8,只存透明度,内存只有 1/4。

一个小技巧:如果你不确定某张图用 RGB_565 会不会翻车,可以在开发阶段做一个 A/B 对比。把两张图并排放在屏幕上,让设计师看一眼。大多数情况下,非照片类图片根本看不出区别。

2.6 别忘了屏幕密度

嗯,这里还有一个坑。上面说的「宽 × 高」是图片本身的像素尺寸。但如果你把图片放到不同密度的设备上,Android 的 BitmapFactory 会根据 inDensity 和 inTargetDensity 自动缩放。

举个例子:

  • 一张 100×100 的图片,放在 mdpi 的 drawable 文件夹
  • 在 xxhdpi 设备上加载
  • Android 会把它放大到 100 × 3 = 300px
  • 内存变成 300 × 300 × 4 = 360KB,而不是 100 × 100 × 4 = 40KB

我曾经排查过一个 OOM 问题,发现就是因为图片放在 mdpi 目录,而测试机是 xxxhdpi,内存膨胀了 9 倍。所以,图片资源要放在正确的密度目录下,或者用 inSampleSize 主动降采样。

避坑:不要把大图放在 drawable(无后缀)或 mdpi 目录下。否则在高密度设备上,内存会成倍增长。我见过一个案例,一张 500KB 的 PNG 放在 mdpi,在 xxxhdpi 设备上解码后占了 50MB 内存——这就是典型的「文件小、内存大」陷阱。

2.7 小结

记住这个公式就够了:内存 = 宽 × 高 × BPP。BPP 的选择直接影响内存占用和画质。ARGB_8888 是安全牌,RGB_565 是省内存利器,但各有适用场景。