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 支持自定义。
相关推荐
南客先生1 小时前
互联网大厂Java面试:RocketMQ、RabbitMQ与Kafka的深度解析
java·面试·kafka·rabbitmq·rocketmq·消息中间件
烛阴1 小时前
JavaScript 的 8 大“阴间陷阱”,你绝对踩过!99% 程序员崩溃瞬间
前端·javascript·面试
宝耶3 小时前
面试常问问题:Java基础篇
java·面试·职场和发展
Tang10245 小时前
Glide 4.x 线程池管理模块解析
面试
Tang10245 小时前
Glide 4.x 三级缓存模块的实现原理
面试
火星思想5 小时前
Promise 核心知识点(非基础)
前端·javascript·面试
uhakadotcom5 小时前
rAthena:快速入门与基础知识详解,附实用示例代码
面试·架构·github
花生了什么树lll6 小时前
面试中被问到过的前端八股(四)
前端·面试
海底火旺6 小时前
破解二维矩阵搜索难题:从暴力到最优的算法之旅
javascript·算法·面试