Glide 4.x 的 Bitmap 资源复用机制详解

结合 Glide 4.x 的源码,深入讲解 Glide 的资源复用机制,重点分析 BitmapPool 如何实现 Bitmap 复用以减少内存分配。Glide 的 BitmapPool 是其内存优化的核心组件,通过复用 Bitmap 对象,显著降低垃圾回收(GC)压力和内存分配开销,尤其在高频图片加载场景(如列表滑动)下效果明显。以下是 BitmapPool 的实现细节、源码分析、复用流程、优化策略以及关键设计亮点。


1. BitmapPool 的作用与设计目标

BitmapPool 是 Glide 用来管理可复用 Bitmap 对象的内存池,主要目标:

  • 减少内存分配 :通过复用已分配的 Bitmap 对象,避免频繁调用 Bitmap.createBitmap
  • 降低 GC 压力 :减少新 Bitmap 对象的创建,降低垃圾回收频率。
  • 优化性能 :复用 Bitmap 比新建 Bitmap 更快,尤其在内存紧张的设备上。

BitmapPool 的核心思想是维护一个 Bitmap 缓存池,当需要新的 Bitmap 时,优先从池中获取符合条件的 Bitmap,而不是直接分配新内存。


2. BitmapPool 的核心接口

BitmapPool 是一个接口,定义了管理 Bitmap 的核心方法:

源码分析:BitmapPool 接口

java 复制代码
public interface BitmapPool {
    // 将 Bitmap 放入池中
    void put(Bitmap bitmap);

    // 获取指定宽高和配置的 Bitmap
    @Nullable
    Bitmap get(int width, int height, Bitmap.Config config);

    // 获取或创建指定宽高和配置的 Bitmap
    @NonNull
    Bitmap getDirty(int width, int height, Bitmap.Config config);

    // 清理池中的所有 Bitmap
    void clearMemory();

    // 修剪池的内存到指定大小
    void trimMemory(int level);
}
  • 方法说明
    • put(Bitmap):将不再使用的 Bitmap 放入池中,供后续复用。
    • get(width, height, config):尝试从池中获取匹配尺寸和配置的 Bitmap,返回 null 如果没有合适的。
    • getDirty(width, height, config):获取一个 Bitmap,如果池中没有则新建一个。
    • clearMemory():清空池中的所有 Bitmap,通常在内存压力大时调用。
    • trimMemory(level):根据内存级别(如 TRIM_MEMORY_MODERATE)释放部分 Bitmap

默认实现

Glide 默认使用 LruBitmapPool 实现 BitmapPool,基于 LRU(Least Recently Used)算法管理 Bitmap


3. LruBitmapPool 的实现细节

LruBitmapPool 是 Glide 的默认 BitmapPool 实现,结合 LRU 算法和尺寸匹配策略管理 Bitmap

源码分析:LruBitmapPool

java 复制代码
public class LruBitmapPool implements BitmapPool {
    private final LruPoolStrategy strategy;
    private final Set<Bitmap.Config> allowedConfigs;
    private final long maxSize;
    private long currentSize;

    public LruBitmapPool(long maxSize) {
        this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
    }

    @Override
    public synchronized void put(Bitmap bitmap) {
        if (bitmap == null) {
            return;
        }
        if (!bitmap.isMutable()) {
            bitmap.recycle();
            return;
        }
        if (!allowedConfigs.contains(bitmap.getConfig())) {
            bitmap.recycle();
            return;
        }
        final int size = Util.getBitmapByteSize(bitmap);
        if (size > maxSize) {
            bitmap.recycle();
            return;
        }

        strategy.put(bitmap);
        currentSize += size;
        evict();
    }

    @Override
    @Nullable
    public synchronized Bitmap get(int width, int height, Bitmap.Config config) {
        Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
        if (result != null) {
            currentSize -= Util.getBitmapByteSize(result);
        }
        return result;
    }

    private void evict() {
        while (currentSize > maxSize) {
            Bitmap removed = strategy.removeLast();
            if (removed == null) {
                currentSize = 0;
                return;
            }
            currentSize -= Util.getBitmapByteSize(removed);
            removed.recycle();
        }
    }
}
  • 关键字段

    • strategyLruPoolStrategy 接口的实现,负责 Bitmap 的存储和查找逻辑,默认是 SizeConfigStrategy
    • allowedConfigs:允许的 Bitmap.Config(如 ARGB_8888RGB_565),默认只允许可变(mutable)的配置。
    • maxSize:池的最大内存大小,默认基于设备内存计算(通常为内存的 1/8)。
    • currentSize:当前池中 Bitmap 占用的内存大小。
  • 核心逻辑

    1. put(Bitmap)
      • 检查 Bitmap 是否为 null、是否可变(isMutable)、是否符合 allowedConfigs、是否超出 maxSize
      • 如果不满足条件,直接调用 bitmap.recycle() 回收。
      • 否则,通过 strategy.put(bitmap) 存入池中,更新 currentSize
      • 调用 evict() 确保池大小不超过 maxSize,移除最久未使用的 Bitmap
    2. get(width, height, config)
      • 通过 strategy.get(width, height, config) 查找匹配的 Bitmap
      • 如果找到,更新 currentSize 并返回。
      • 如果未找到,返回 null
    3. evict()
      • currentSize 超过 maxSize 时,循环调用 strategy.removeLast() 移除最旧的 Bitmap,并回收。

LruPoolStrategy

LruPoolStrategy 是一个接口,定义了 Bitmap 的存储、获取和移除逻辑,默认实现是 SizeConfigStrategy

源码分析:SizeConfigStrategy
java 复制代码
class SizeConfigStrategy implements LruPoolStrategy {
    private final KeyPool keyPool = new KeyPool();
    private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
    private final SortedMap<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new EnumMap<>(Bitmap.Config.class);

    @Override
    public void put(Bitmap bitmap) {
        int size = Util.getBitmapByteSize(bitmap);
        Key key = keyPool.get(size, bitmap.getConfig());
        groupedMap.put(key, bitmap);

        NavigableMap<Integer, Integer> sizes = sortedSizes.get(bitmap.getConfig());
        if (sizes == null) {
            sizes = new TreeMap<>();
            sortedSizes.put(bitmap.getConfig(), sizes);
        }
        Integer current = sizes.get(size);
        sizes.put(size, current == null ? 1 : current + 1);
    }

    @Override
    @Nullable
    public Bitmap get(int width, int height, Bitmap.Config config) {
        int size = Util.getBitmapByteSize(width, height, config);
        Key key = keyPool.get(size, config);
        Bitmap result = groupedMap.get(key);
        if (result != null) {
            decrementSize(result.getConfig(), Util.getBitmapByteSize(result));
            return result;
        }

        NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);
        if (sizes != null) {
            Map.Entry<Integer, Integer> entry = sizes.ceilingEntry(size);
            if (entry != null && entry.getKey() <= size * MAX_SIZE_MULTIPLE) {
                key = keyPool.get(entry.getKey(), config);
                result = groupedMap.get(key);
                if (result != null) {
                    decrementSize(config, entry.getKey());
                }
            }
        }
        return result;
    }

    @Override
    @Nullable
    public Bitmap removeLast() {
        Bitmap removed = groupedMap.removeLast();
        if (removed != null) {
            int size = Util.getBitmapByteSize(removed);
            decrementSize(removed.getConfig(), size);
        }
        return removed;
    }
}
  • 核心数据结构

    • groupedMapGroupedLinkedMap<Key, Bitmap>,存储 Bitmap 和对应的 Key,基于 LRU 顺序。
    • sortedSizesEnumMap<Config, NavigableMap<Integer, Integer>>,按 Config 和大小存储 Bitmap 的计数。
    • keyPoolKeyPool,缓存 Key 对象,减少对象创建。
    • Key:包含 Bitmap 的大小(字节)和 Config,用于查找匹配的 Bitmap
  • 核心逻辑

    1. put(Bitmap)
      • 计算 Bitmap 的字节大小(width * height * bytesPerPixel)。
      • 创建 Key(包含大小和 Config),存入 groupedMap
      • 更新 sortedSizes,记录该大小和 ConfigBitmap 计数。
    2. get(width, height, config)
      • 计算请求的 Bitmap 大小。
      • 优先查找精确匹配的 Key(大小和 Config 相同)。
      • 如果未找到,查找稍大的 Bitmap(大小不超过请求大小的 MAX_SIZE_MULTIPLE 倍,默认为 8)。
      • 返回找到的 Bitmap,并更新计数。
    3. removeLast()
      • groupedMap 移除最久未使用的 Bitmap
      • 更新 sortedSizes 的计数。
  • 关键优化

    • 尺寸模糊匹配 :允许获取稍大的 Bitmap,通过 ceilingEntry 查找接近但不小于请求大小的 Bitmap,减少浪费。
    • Config 隔离 :按 Bitmap.Config 分组存储,避免不同格式的 Bitmap 混淆。
    • LRU 管理GroupedLinkedMap 确保最久未使用的 Bitmap 优先被移除。

4. Bitmap 复用流程

以下是 BitmapPool 在图片加载中的完整复用流程:

  1. 加载请求触发

    • Engine.load() 创建 DecodeJob,准备加载图片。
    • DecodeJob 调用 Decoder(如 StreamBitmapDecoder)解码数据。
  2. Bitmap 获取

    • 解码器通过 BitmapPool.get(width, height, config) 请求 Bitmap
    • 如果池中有匹配的 Bitmap,直接返回并重用。
    • 如果池中没有,调用 Bitmap.createBitmap(width, height, config) 创建新 Bitmap
  3. Bitmap 使用

    • 解码器将数据解码到复用的 Bitmap 上(如通过 BitmapFactory.decodeStream)。
    • 解码后的 Bitmap 包装为 Resource<Bitmap>,传递给 Target
  4. Bitmap 释放

    • Resource 不再需要时(Target 调用 clear 或资源被缓存替换),调用 Resource.recycle()
    • Bitmap 通过 BitmapPool.put(bitmap) 放回池中。
  5. 内存管理

    • 如果池大小超过 maxSizeevict() 移除最旧的 Bitmap 并调用 bitmap.recycle()
    • 在内存压力下,clearMemory()trimMemory() 清空或修剪池。

源码分析:StreamBitmapDecoder

java 复制代码
public class StreamBitmapDecoder implements ResourceDecoder<InputStream, Bitmap> {
    private final BitmapPool bitmapPool;
    private final ArrayPool byteBufferPool;

    @Override
    public Resource<Bitmap> decode(
            @NonNull InputStream source, int width, int height, @NonNull Options options)
            throws IOException {
        BitmapFactory.Options bitmapOptions = getBitmapOptions();
        bitmapOptions.inBitmap = bitmapPool.get(width, height, bitmapOptions.inPreferredConfig);
        try {
            Bitmap bitmap = BitmapFactory.decodeStream(source, null, bitmapOptions);
            return BitmapResource.obtain(bitmap, bitmapPool);
        } finally {
            if (bitmapOptions.inBitmap != null) {
                bitmapPool.put(bitmapOptions.inBitmap);
            }
        }
    }
}
  • 作用StreamBitmapDecoder 使用 BitmapPool 获取 Bitmap 进行解码。
  • 关键点
    • bitmapOptions.inBitmap 设置复用的 BitmapBitmapFactory 将数据解码到此 Bitmap
    • 如果解码失败,inBitmap 放回池中,避免浪费。
    • 解码后的 Bitmap 包装为 BitmapResource,由 BitmapPool 管理生命周期。

5. 优化策略与设计亮点

  1. 尺寸模糊匹配

    • SizeConfigStrategy 允许获取稍大的 Bitmap,通过 MAX_SIZE_MULTIPLE(默认 8)限制匹配范围,平衡复用率和内存浪费。

    • 源码:

      java 复制代码
      if (entry.getKey() <= size * MAX_SIZE_MULTIPLE) {
          // 使用稍大的 Bitmap
      }
  2. Config 隔离

    • Bitmap.Config 分组存储(如 ARGB_8888RGB_565 单独管理),避免格式不匹配。
    • 默认只支持可变(mutable)的 Bitmap,确保可以复用。
  3. LRU 管理

    • 使用 GroupedLinkedMap 实现 LRU 淘汰,优先移除最久未使用的 Bitmap
    • 通过 sortedSizes 快速查找匹配的 Bitmap
  4. 内存动态调整

    • maxSize 根据设备内存动态计算(参考 MemorySizeCalculator)。
    • 支持 trimMemoryclearMemory,响应系统内存压力。
  5. 线程安全

    • LruBitmapPool 的核心方法使用 synchronized 确保线程安全。
    • KeyPool 缓存 Key 对象,减少同步操作的开销。
  6. 异常处理

    • 如果 Bitmap 不可变或格式不支持,直接回收,避免池中存储无效对象。
    • 解码失败时,inBitmap 放回池中,确保资源不浪费。

6. 常见问题与解决方案

  1. 复用率低

    • 问题Bitmap 尺寸或 Config 不匹配,导致频繁创建新 Bitmap
    • 解决方案
      • 统一图片加载的 Config(如默认 ARGB_8888)。

      • 调整 MAX_SIZE_MULTIPLE,允许更大范围的尺寸匹配。

      • 使用 GlideBuilder 增大 BitmapPool 大小:

        java 复制代码
        new GlideBuilder().setBitmapPool(new LruBitmapPool(maxSize * 2));
  2. 内存泄漏

    • 问题Bitmap 未正确放回池中,导致内存占用增加。
    • 解决方案
      • 确保 Resource.recycle() 被调用(通过 RequestManager 绑定生命周期)。
      • 检查自定义 Decoder 是否正确处理 Bitmap
  3. 解码失败

    • 问题 :复用的 inBitmap 导致 BitmapFactory 解码失败(Android 4.4+ 对 inBitmap 有严格要求)。
    • 解决方案
      • 使用 DownsamplerinBitmap 兼容逻辑,确保尺寸和格式匹配。

      • 源码:

        java 复制代码
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            bitmapOptions.inBitmap = bitmapPool.get(width, height, config);
        }
  4. 池大小不足

    • 问题maxSize 过小,导致频繁回收 Bitmap
    • 解决方案
      • 通过 GlideBuilder 增加池大小:

        java 复制代码
        new GlideBuilder().setBitmapPool(new LruBitmapPool(maxSize * 1.5f));

7. 源码中的典型调用链

以下是 BitmapPool 的典型使用链:

java 复制代码
// DecodeJob 调用 Decoder
Resource<Bitmap> resource = decoder.decode(source, width, height, options);

// StreamBitmapDecoder 获取 Bitmap
Bitmap bitmap = bitmapPool.get(width, height, config);
bitmapOptions.inBitmap = bitmap;
Bitmap result = BitmapFactory.decodeStream(source, null, bitmapOptions);

// BitmapResource 管理 Bitmap
return BitmapResource.obtain(result, bitmapPool);

// Resource 释放时放回池中
public class BitmapResource implements Resource<Bitmap> {
    @Override
    public void recycle() {
        if (bitmap != null) {
            bitmapPool.put(bitmap);
        }
    }
}

8. 扩展与自定义

开发者可以通过以下方式自定义 BitmapPool

  1. 自定义池大小

    java 复制代码
    long customMaxSize = Runtime.getRuntime().maxMemory() / 4;
    new GlideBuilder().setBitmapPool(new LruBitmapPool(customMaxSize));
  2. 自定义 LruPoolStrategy

    • 实现 LruPoolStrategy,修改 Bitmap 的匹配逻辑(如更宽松的尺寸匹配)。

    • 示例:

      java 复制代码
      class CustomStrategy implements LruPoolStrategy {
          @Override
          public Bitmap get(int width, int height, Bitmap.Config config) {
              // 自定义匹配逻辑
          }
      }
      new GlideBuilder().setBitmapPool(new LruBitmapPool(maxSize, new CustomStrategy()));
  3. 禁用 BitmapPool

    • 使用空实现:

      java 复制代码
      new GlideBuilder().setBitmapPool(new BitmapPool() {
          @Override
          public void put(Bitmap bitmap) {
              bitmap.recycle();
          }
          @Override
          public Bitmap get(int width, int height, Bitmap.Config config) {
              return null;
          }
          // ...
      });

9. 总结

Glide 的 BitmapPool 通过 LruBitmapPoolSizeConfigStrategy 实现高效的 Bitmap 复用:

  • 核心机制 :维护 LRU 管理的 Bitmap 池,支持尺寸和 Config 匹配。
  • 流程 :解码时从池中获取 Bitmap,释放时放回池中,超出大小自动回收。
  • 优化:尺寸模糊匹配、Config 隔离、线程安全和动态内存管理。
  • 优势:显著减少内存分配和 GC 压力,适合高频加载场景。
  • 灵活性 :通过 GlideBuilderLruPoolStrategy 支持自定义。
相关推荐
uhakadotcom40 分钟前
在chrome浏览器插件之中,options.html和options.js常用来做什么事情
前端·javascript·面试
想想就想想40 分钟前
线程池执行流程详解
面试
程序员清风2 小时前
Dubbo RPCContext存储一些通用数据,这个用手动清除吗?
java·后端·面试
南北是北北2 小时前
JetPack ViewBinding
面试
南北是北北2 小时前
jetpack ViewModel
面试
渣哥3 小时前
Lazy能否有效解决循环依赖?答案比你想的复杂
javascript·后端·面试
前端架构师-老李4 小时前
面试问题—你接受加班吗?
面试·职场和发展
ANYOLY4 小时前
多线程&并发篇面试题
java·面试
南北是北北4 小时前
RecyclerView 的数据驱动更新
面试
uhakadotcom4 小时前
coze的AsyncTokenAuth和coze的TokenAuth有哪些使用的差异?
后端·面试·github