四、内存复用技术:inBitmap复用、BitmapFactory.Options.inBitmap、复用池管理
好,咱们接着聊。前面几章我们把 Bitmap 的加载、缓存、回收都捋了一遍。但有个问题一直悬着——你频繁创建和销毁 Bitmap,GC 压力大不大?肯定大。那有没有办法让 Bitmap 像快递箱一样,用完了回收,下次再用?
有的。这就是 inBitmap 复用技术。
说白了,就是让 Bitmap 对象别急着死,把它的内存空间腾出来给下一张图用。我当年在做一个图片编辑 App 时,用户连续切换滤镜,Bitmap 像雪花一样创建又销毁,内存抖得跟心电图似的。后来上了 inBitmap 复用,GC 频率直接降了一半。
4.1 inBitmap 的基本原理
先看一个关键字段:BitmapFactory.Options.inBitmap。
这个字段是干嘛的?你加载一张新图片时,告诉解码器:“别给我 new 内存了,用这块现成的。” 解码器会检查这块内存能不能装下新图,能的话就直接复用。
嗯,这里要注意——不是随便一张 Bitmap 都能当 inBitmap 用。有几个硬性条件:
- Android 4.4(API 19)以上才支持。4.4 之前只能复用相同尺寸的。
- 复用 Bitmap 必须是可变(mutable)的。你加载资源文件时默认是不可变的,得用
BitmapFactory.Options.inMutable = true。 - 新图片的字节数不能超过复用 Bitmap 的字节数。说白了,你拿个 1MB 的坑,想装 2MB 的图,装不下。
核心要点:inBitmap 复用的是 Bitmap 的 像素内存,不是 Bitmap 对象本身。对象还在,但里面的像素数据被覆盖了。
4.2 代码实战:怎么用 inBitmap
我直接给一段我项目里用过的代码。你感受一下:
// 1. 准备一个可复用的 Bitmap 池
private Bitmap mReusableBitmap;
// 2. 加载图片时启用复用
public Bitmap loadBitmapWithReuse(Resources res, int resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 关键:开启复用
options.inMutable = true;
options.inJustDecodeBounds = false;
// 如果有可复用的 Bitmap,就设置进去
if (mReusableBitmap != null) {
options.inBitmap = mReusableBitmap;
}
Bitmap bitmap = BitmapFactory.decodeResource(res, resId, options);
// 如果复用失败,会返回 null,需要重新创建
if (bitmap == null) {
// 复用失败,走正常流程
options.inBitmap = null;
bitmap = BitmapFactory.decodeResource(res, resId, options);
}
// 更新复用池
mReusableBitmap = bitmap;
return bitmap;
}
你看,逻辑其实不复杂。但有个坑——复用失败时 decode 会返回 null。我曾经因为这个 bug 排查了一下午,界面上一片空白,还以为是图片没加载出来。后来才发现是 inBitmap 设置了一个不兼容的 Bitmap,解码器直接罢工了。
避坑指南:设置 inBitmap 后,一定要检查 decode 结果是否为 null。如果为 null,去掉 inBitmap 重新解码一次。这是官方推荐的兜底策略。
4.3 复用池管理:从单兵作战到集团军
单个 Bitmap 复用太局限了。你想想看,如果只有一张可复用的 Bitmap,那同一时间只能处理一张图片。多线程加载时怎么办?
所以我们需要一个 复用池。说白了就是一个集合,里面存着多个可复用的 Bitmap,按需取用。
我习惯用 LinkedBlockingQueue 或者 ArrayList 来管理。但要注意,复用池不是越大越好。池子太大,内存占用反而更高。我一般控制在 3~5 个,具体看应用场景。
来看一个简单的复用池实现:
public class BitmapReusePool {
private static final int MAX_POOL_SIZE = 4;
private final List<Bitmap> mPool = new ArrayList<>(MAX_POOL_SIZE);
// 从池中取一个可复用的 Bitmap
public Bitmap getReusableBitmap(int width, int height, Bitmap.Config config) {
for (int i = 0; i < mPool.size(); i++) {
Bitmap bitmap = mPool.get(i);
if (canReuse(bitmap, width, height, config)) {
mPool.remove(i);
return bitmap;
}
}
return null;
}
// 把用过的 Bitmap 放回池中
public void putBitmap(Bitmap bitmap) {
if (bitmap != null && bitmap.isMutable()) {
if (mPool.size() < MAX_POOL_SIZE) {
mPool.add(bitmap);
} else {
bitmap.recycle(); // 池满了,直接回收
}
}
}
// 判断能否复用
private boolean canReuse(Bitmap bitmap, int width, int height, Bitmap.Config config) {
if (bitmap == null || bitmap.isRecycled()) return false;
// Android 4.4 以上:只要新图字节数 <= 复用图字节数即可
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int newSize = width * height * getBytesPerPixel(config);
return newSize <= bitmap.getAllocationByteCount();
}
// 4.4 以下:必须宽高和 Config 完全一致
return bitmap.getWidth() == width
&& bitmap.getHeight() == height
&& bitmap.getConfig() == config;
}
private int getBytesPerPixel(Bitmap.Config config) {
switch (config) {
case ARGB_8888: return 4;
case RGB_565: return 2;
case ARGB_4444: return 2;
case ALPHA_8: return 1;
default: return 4;
}
}
}
个人经验:复用池里的 Bitmap 最好按尺寸分组。比如 100x100 的一组,200x200 的一组。这样取用时匹配度更高,避免大材小用或者小材大用。
4.4 复用策略的取舍
inBitmap 不是万能的。我总结了几条经验:
| 场景 | 建议 | 原因 |
|---|---|---|
| 图片尺寸变化频繁 | 慎用复用池 | 尺寸不匹配会导致复用失败率高,反而增加开销 |
| 图片尺寸相对固定 | 强烈推荐 | 比如列表中的头像、缩略图,复用效果极佳 |
| 大图加载 | 建议复用 | 大图内存占用高,复用能显著减少 GC 压力 |
| 多线程并发加载 | 需要加锁 | 复用池是共享资源,多线程访问要同步 |
另外,复用不等于零开销。每次复用都要做兼容性检查,池子管理也有锁竞争。如果你的图片加载量不大,或者内存本身很充裕,没必要硬上复用。
我记得有一次优化一个新闻客户端,图片列表加载很流畅,但上了复用池后反而卡顿了。排查下来发现是锁竞争太严重,去掉复用池后反而更顺。所以说,技术选型要因地制宜。
4.5 总结一下
inBitmap 复用,说白了就是让 Bitmap 内存循环利用。核心三点:
- 设置 inBitmap 字段,告诉解码器用现成的内存
- 管理复用池,按尺寸分组,控制池大小
- 处理复用失败,兜底策略不能少
下一章我们会聊 Bitmap 的压缩策略。到时候你会看到,复用和压缩结合起来,才是真正的内存管理大招。