四、内存复用技术: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 的压缩策略。到时候你会看到,复用和压缩结合起来,才是真正的内存管理大招。