第2章:内存计算基础——Bitmap到底吃了多少内存?

好,咱们直接进入正题。上一章我讲了Bitmap的整个生命周期,这一章我们来算一笔账——Bitmap到底占了多少内存?

你可能觉得这很简单,不就是宽×高×每个像素的字节数吗?嗯,对了一半。但实际项目中,我见过太多人栽在这个“简单”的公式上。

2.1 核心公式:一张图的内存怎么算?

先上公式,这个你得刻在脑子里:

Bitmap内存 ≈ 宽度 × 高度 × 每个像素占用的字节数

等等,这里有个坑——宽度和高度是像素单位,不是dp,也不是英寸。我当年刚入行时,就犯过这个错,把布局里的dp值直接当像素用了,结果算出来的内存差了十万八千里。

举个例子:一张 1920×1080 的图片,用 ARGB_8888 格式加载:

内存 = 1920 × 1080 × 4 = 8,294,400 字节 ≈ 7.9 MB

一张图就吃掉将近8MB。你想想看,如果列表里加载20张这样的图,光图片就占160MB。嗯,这就是OOM的常见原因之一。

核心要点:Bitmap内存 = 像素总数 × 每像素字节数。这个公式是基础,但实际值还会受像素密度影响,下面细说。

2.2 宽高与像素密度的关系——这个坑我踩过

很多新手会问:我图片是100×100的,放到hdpi和xxhdpi设备上,内存一样吗?

不一样,而且差别很大。

Android系统在加载资源图片时,会根据设备密度自动缩放。具体规则是这样的:

  • mdpi(160dpi):基准密度,缩放系数 = 1.0
  • hdpi(240dpi):缩放系数 = 1.5
  • xhdpi(320dpi):缩放系数 = 2.0
  • xxhdpi(480dpi):缩放系数 = 3.0
  • xxxhdpi(640dpi):缩放系数 = 4.0

实际加载时,Bitmap的宽高会被乘以这个系数。公式变成:

实际宽度 = 原始宽度 × (设备密度 / 资源所在文件夹的基准密度)
实际高度 = 原始高度 × (设备密度 / 资源所在文件夹的基准密度)

举个例子你就明白了:

一张 100×100 的图片,放在 drawable-mdpi 文件夹里。在 xxhdpi(480dpi)设备上加载:

实际宽度 = 100 × (480 / 160) = 300 像素
实际高度 = 100 × (480 / 160) = 300 像素
内存 = 300 × 300 × 4 = 360,000 字节 ≈ 351 KB

同样的图片,如果放在 drawable-xxhdpi 文件夹里:

实际宽度 = 100 × (480 / 480) = 100 像素
实际高度 = 100 × (480 / 480) = 100 像素
内存 = 100 × 100 × 4 = 40,000 字节 ≈ 39 KB

看到了吗?同一个图片,放在不同密度的文件夹里,内存差了将近9倍!

避坑指南:我曾经接手过一个项目,所有图片都扔在drawable-mdpi里,结果在高端机型上频繁OOM。后来一查,图片被放大了3倍,内存暴涨。所以,图片一定要放在对应密度的文件夹里,或者直接用 drawable-nodpi 禁止缩放。

2.3 不同格式的内存对比——选对格式省一半内存

Android中Bitmap支持多种格式,每种格式每个像素占用的字节数不同。我整理了一张表,你直接收藏:

格式 每像素字节数 支持透明度 适用场景
ARGB_8888 4字节 默认格式,色彩丰富,质量最高
RGB_565 2字节 无透明需求的图片,省一半内存
ARGB_4444 2字节 已废弃,质量差,别用了
ALPHA_8 1字节 仅透明度 只有透明度信息的图片,如蒙版

来算一笔账:一张 1920×1080 的图片:

  • ARGB_8888:1920 × 1080 × 4 = 7.9 MB
  • RGB_565:1920 × 1080 × 2 = 3.95 MB

直接省了一半!如果你的图片不需要透明度,比如JPEG格式的图片,用RGB_565完全够用。我做过一个相册App,所有缩略图都用RGB_565,内存占用直接砍半,用户体验提升明显。

小技巧:在Android 4.4(API 19)以上,BitmapFactory.Options 的 inPreferredConfig 可以指定加载格式。我习惯这样用:

options.inPreferredConfig = Bitmap.Config.RGB_565;

但要注意,如果原图有透明度,RGB_565会丢失透明通道,显示为黑色背景。所以,有透明需求的图片还是用ARGB_8888

2.4 实战中的内存估算——别等OOM了才后悔

说了这么多理论,咱们来点实际的。你在开发时,怎么快速估算一张图的内存?

我个人习惯用这个公式:

内存(MB) ≈ (宽度 × 高度 × 每像素字节数) / (1024 × 1024)

举个例子,一张 2000×1500 的图片,ARGB_8888格式:

内存 ≈ (2000 × 1500 × 4) / 1,048,576 ≈ 11.4 MB

嗯,一张图就11MB。如果你的App可用内存只有64MB,那同时显示5张这样的图就危险了。

我建议你在开发阶段,就加上内存监控:

// 获取Bitmap的内存大小(API 12以上)
int size = bitmap.getByteCount();

// API 19以上推荐用这个
int size = bitmap.getAllocationByteCount();

这两个方法的区别在于:getByteCount() 返回的是图片实际占用的内存,而 getAllocationByteCount() 返回的是系统分配的内存。在复用Bitmap时,后者可能更大。我一般用 getAllocationByteCount(),因为它更接近真实的内存占用。

总结一下这一章的核心:

  1. Bitmap内存 = 宽 × 高 × 每像素字节数
  2. 像素密度会导致图片缩放,注意资源文件夹的选择
  3. RGB_565比ARGB_8888省一半内存,适合无透明度的图片
  4. 开发时用 getAllocationByteCount() 监控内存

下一章,我会讲Bitmap的加载策略——如何优雅地加载大图,避免OOM。到时候我会分享一个我压箱底的加载框架设计思路,敬请期待。