Bitmap 对象的创建、复用与回收机制
做图片加载优化这么多年,我见过最头疼的问题就是——Bitmap 对象的生命周期管理。说白了,就是三个核心动作:怎么建、怎么用、怎么丢。大部分 OOM 都出在这几个环节上。
1. Bitmap 的创建:没那么简单
创建 Bitmap 最常见的方式就是通过 BitmapFactory。但你真的了解每个参数背后的坑吗?
| 方法 | 适用场景 | 我遇到的坑 |
|---|---|---|
| BitmapFactory.decodeFile() | 加载本地文件 | 不处理缩放,大图直接爆内存 |
| BitmapFactory.decodeResource() | 加载 res 资源 | 不同 dpi 目录会自动缩放,容易尺寸翻倍 |
| BitmapFactory.decodeStream() | 加载网络流 | 流必须支持 mark/reset,否则采样失败 |
| BitmapFactory.decodeByteArray() | 加载 byte[] | 记得先解压缩,否则浪费内存 |
我个人习惯在 decode 之前,先通过 inJustDecodeBounds 获取图片的真实宽高,然后再根据目标控件尺寸计算采样比例。举个例子:
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, opts);
int outWidth = opts.outWidth;
int outHeight = opts.outHeight;
// 计算采样比例
opts.inSampleSize = calculateInSampleSize(outWidth, outHeight, 200, 300);
opts.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(path, opts);
2. Bitmap 的复用:别频繁创建对象
创建 Bitmap 对象本身就很耗时,GC 频繁触发还会导致卡顿。所以复用机制很重要。Android 3.0 之后提供了 BitmapFactory.Options.inBitmap,让我们可以重复使用一块内存。
我记得有个项目是做瀑布流图片列表,用户快速滑动时一秒要加载十几张图。如果不复用,GC 每秒钟触发好几次,列表直接掉帧。
2.1 复用要求
- Android 4.4 之前:复用 Bitmap 必须与被复用对象 尺寸一致
- Android 4.4 及以上:只需 被复用对象的大小 ≥ 新图所需内存(且像素格式相同)
- inMutable 必须设为 true
// 准备一个复用池,典型的 LruCache 实现
private fun getReusableBitmap(width: Int, height: Int): Bitmap? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return reusableCache?.let { cache ->
cache.keys.forEach { key ->
val bmp = cache[key]
if (bmp != null && bmp.width * bmp.height * 4 >= width * height * 4) {
cache.remove(key)
return bmp
}
}
}
}
// 老版本需要严格匹配尺寸
// ...
return null
}
3. Bitmap 的回收:该放手时就放手
为什么还要手动回收?Android 系统垃圾回收线程不是会自动处理吗?
嗯,理论上是的。但实际项目中,Bitmap 对象特别大,GC 回收速度赶不上创建速度。尤其在低端机上,GC 每次回收都要触发 Stop The World,卡顿感非常明显。
我曾经在一个直播项目里遇到过:用户连麦时频繁切换摄像头画面,每秒钟创建 10 张 1080p 的 Bitmap,GC 来不及回收,内存直接飙到 200MB 以上,然后崩溃。
3.1 什么时候必须回收
- 使用
Bitmap.createBitmap()创建的 Bitmap,必须在不再需要时调用 recycle() - 从 BitmapFactory 取出的 Bitmap,如果共享了同一个 byte[],不能随意回收
- Android 3.0 之前,原生堆无法追踪,必须手动回收
3.2 正确的回收姿势
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null; // 记得清引用,否则 GC 依然持有
}
Canvas: trying to use a recycled bitmap 异常。正确的做法是在 确认不再绘制后 再回收,比如在 Activity 的 onDestroy 或自定义 View 的 onDetachedFromWindow 中。
4. 全生命周期管理:从创建到回收
把上面三个环节串起来,就是一套完整的 Bitmap 管理流程。我画了一张图,你可以对照着看:
5. 实战中容易忽略的细节
5.1 在 ListView 或 RecyclerView 中
- 不要在 getView 里频繁新建 Bitmap,用图片加载库(Glide/Picasso)内部已经做了复用
- 如果自己管理,一定要在 Item 被回收时释放图片引用
5.2 在 Canvas 中直接绘制
// 错误示范
Canvas canvas = new Canvas(bitmap);
// ... 画完之后
bitmap.recycle(); // ❌ 此时 Canvas 还在引用,画布内容就没啦
// 正确做法
Canvas canvas = new Canvas(bitmap);
// ... 画完之后,确认不再使用
canvas = null; // 先断开 Canvas 引用
bitmap.recycle(); ✅
5.3 多线程加载
我个人习惯在异步线程 decode 完成后,通过 Handler 切回主线程设置。这样 decode 不会卡 UI,同时 Bitmap 的回收也可以放在后台线程——但要注意回收操作本身不是线程安全的,最好用一个串行的 HandlerThread 来处理。
6. Android 版本带来的差异
| Android 版本 | Bitmap 存储位置 | 回收策略 |
|---|---|---|
| 2.x (API < 12) | Java 堆 | 必须手动 recycle(),否则容易 OOM |
| 3.0 ~ 7.1 | Java 堆 | 建议手动回收,但 inBitmap 可用(API 11+) |
| 8.0 及以上 | Native 堆 | GC 会自动回收,但复用仍然有价值;inBitmap 依然推荐 |