Android Glide 笔记

18 阅读12分钟

关于Glide,可以说是我们这行里普及率最高的图片加载框架了。它不仅仅是加载一张图片那么简单,其背后关于缓存性能生命周期安全以及内存管理的设计,都非常精妙。我们从它的核心设计理念开始,一步步深入到源码实现。

🎯 Glide的核心设计理念:丝滑与安全

Glide的设计目标很明确:在保证应用不卡顿、不崩溃的前提下,用最快的方式把图片显示出来。它主要通过三点来实现:

  1. 极速加载:通过多层缓存机制,让图片加载"一次下载,多次瞬时呈现"。
  2. 内存安全:通过复杂的引用计数和Bitmap池化技术,极力避免OOM。
  3. 生命周期安全:自动绑定页面的生命周期,避免在后台或已销毁的页面继续加载图片而浪费资源。

🗺️ Glide的三层缓存:不仅仅是三级缓存

很多老项目会自己实现一个"内存-磁盘-网络"的三级缓存。但Glide的缓存机制要精妙得多,它在内存层面又细分为了两层,形成了一个 "活动缓存 -> 内存缓存 -> 磁盘缓存 -> 网络" 的四级模型。你可以先通过下面这张流程图,直观地感受一下Glide加载一张图片的完整流程:

flowchart TD
    A[开始加载图片] --> B{活动缓存<br>ActiveResources};
    B -- 命中 --> C[直接返回图片<br>并更新引用计数];
    B -- 未命中 --> D{内存缓存<br>LruResourceCache};
    D -- 命中 --> E[将图片从内存缓存<br>移入活动缓存];
    E --> C;
    D -- 未命中 --> F{磁盘缓存<br>DiskLruCache};
    F -- 命中 --> G[解码图片];
    G --> H[将图片放入活动缓存];
    H --> C;
    F -- 未命中 --> I[从网络加载];
    I --> J[解码图片];
    J --> K[写入磁盘缓存];
    K --> H;

下面我们来逐一拆解这其中的关键环节。

  • 第一层:活动缓存 (Active Resources)

    • 是什么:这是一个使用弱引用实现的缓存池,缓存的是当前屏幕上正在被使用的图片。
    • 为什么需要它:设想一下,当你快速滑动RecyclerView时,很多图片正在被展示。如果这些图片同时存在于可被随意淘汰的LruCache中,就有可能因为LruCache策略而被错误地回收,导致滑回来时又要重新加载。Glide通过弱引用将这些正在使用的图片保护起来,告诉GC:"这些图片正在用,别动它们"。
    • 实现原理:每个图片资源(EngineResource)内部都有一个引用计数器。当ImageView显示它时,计数器+1;当ImageView被复用时,计数器-1。当计数器为0时,说明图片不再被任何View引用,它就会被从活动缓存中移除,并送入下一层的内存缓存。
  • 第二层:内存缓存 (Memory Cache)

    • 是什么:采用 LruCache(最近最少使用) 算法实现的缓存。它缓存的是经过转换后、与目标ImageView尺寸相匹配的图片,而不是原始大图。这也是Glide比Picasso更省内存的关键原因之一。
    • 工作机制:当一张图片不再被任何View使用(从活动缓存中移除)时,它会被放入LruCache。当LruCache的大小达到设定的阈值时,它会将最久未使用的图片移除。但这里的"移除"并非简单的丢弃,而是将图片本身(Bitmap)回收到一个专门的 Bitmap池 (BitmapPool) 中。这个设计非常巧妙,避免了频繁的内存申请和GC,极大地提升了性能。
  • 第三层:磁盘缓存 (Disk Cache)

    • 是什么:基于DiskLruCache实现,将图片文件缓存到应用的私有目录。
    • 灵活的缓存策略:你可以通过diskCacheStrategy()方法设置多种策略:
      • DiskCacheStrategy.RESULT(默认) 只缓存转换后的图片(最终显示的样子)。
      • DiskCacheStrategy.SOURCE:只缓存原始图片。
      • DiskCacheStrategy.ALL:缓存原始和转换后的所有图片。
      • DiskCacheStrategy.NONE:不缓存到磁盘。
    • 缓存Key的生成:磁盘缓存的Key生成规则非常严格,它由urlsignaturewidthheighttransformation(变换)、encoder等十多个参数共同决定。这意味着,即使URL相同,如果指定的加载尺寸或变换效果不同,Glide也会将其视为不同的图片来缓存。

🕹️ 生命周期管理:看不见的Fragment

这是一个设计上的神来之笔。我们调用Glide.with(this)时,传入的this可能是ActivityFragment。Glide会做以下几件事:

  1. 获取或创建:获取一个RequestManagerRetriever,并根据传入的ActivityFragment,找到一个"看不见的"SupportRequestManagerFragment
  2. 绑定生命周期:将这个透明的Fragment动态地添加到当前的Activity中。
  3. 感知与通知:当Activity/Fragment的生命周期变化(如onStartonStoponDestroy)时,这个透明的Fragment会感知到,并通知对应的RequestManager暂停、恢复或终止正在进行的图片加载请求。 这就完美地解决了在后台线程加载图片,而页面已销毁导致的内存泄漏或崩溃问题。

🚀 高级用法与性能优化

掌握了核心原理,我们再看看实际项目中如何玩转Glide。

  • 1. 图片变换 (Transformations):通过RequestOptionstransform()方法,可以轻松实现圆角、高斯模糊、灰度等效果。其原理是,Glide从缓存或网络拿到图片后,会应用你指定的Transformation,生成一张新图,然后再显示。
  • 2. 预加载 (Preloading):使用.preload()方法,可以提前将图片加载到缓存中。当真正需要显示时,直接从内存缓存中读取,实现秒开。
  • 3. 自定义缓存Key:通过.signature()方法,可以为缓存Key添加一个额外的标识。比如,当你需要更新一张已经缓存的用户头像时,可以在图片URL后拼接一个版本号参数,或者使用.signature(new ObjectKey(System.currentTimeMillis()))来强制刷新。
  • 4. 仅缓存原始图:如果你需要处理大图或支持图片放大功能,可以设置.diskCacheStrategy(DiskCacheStrategy.SOURCE),这样磁盘上存的是原始高清图,而内存中依然只缓存适合屏幕大小的缩略图。

💎 总结

Glide的强大,源于它对细节的极致追求:

  • 性能上,通过活动缓存 + LruCache + BitmapPool的组合拳,既保证了正在使用的图片不被误伤,又复用了Bitmap内存,极大降低了GC压力。
  • 安全上,通过透明Fragment将图片加载与组件生命周期强绑定,杜绝了内存泄漏。
  • 灵活性上,提供了丰富的API和扩展点,让开发者可以精细控制加载的每一个环节。

BitmapPool

🎯 为什么需要 BitmapPool?

在 Android 中,频繁创建和销毁 Bitmap 对象是一个非常昂贵的操作。这不仅涉及 Dalvik/ART 堆内存的分配,还可能导致频繁的 GC(垃圾回收),从而引发界面卡顿。尤其是在列表快速滑动时,大量图片的加载和释放会带来巨大的内存压力。

BitmapPool 的核心思想就是 “重用已分配的内存”。当一个 Bitmap 不再被任何视图引用时,我们不直接将其内存释放(或让其被 GC 回收),而是把它放入一个“对象池”中。当需要一个新的 Bitmap 时,如果池中有符合要求的闲置 Bitmap,就直接拿来复用它的内存空间,从而避免了重新分配内存的开销。

🧠 Bitmap 内存管理的历史背景

要深入理解 BitmapPool,需要先了解 Bitmap 内存模型在不同 Android 版本上的演变:

  • Android 2.3.x (API 10) 及以下:Bitmap 的像素数据存储在 Native 内存中,Bitmap 对象本身(一个小型 Java 对象)存储在 Dalvik 堆中。开发者需要手动调用 recycle() 来释放 Native 内存。
  • Android 3.0 (API 11) ~ Android 7.1 (API 25):Bitmap 的像素数据与 Bitmap 对象一起分配在 Dalvik 堆中。这简化了内存管理(GC 会自动回收),但也带来了更大的 GC 压力,因为每个 Bitmap 都占用大量堆内存。
  • Android 8.0 (API 26) 及以上:Bitmap 的像素数据又重新移回 Native 堆,但 Bitmap 对象本身仍在 Java 堆。分配和释放 Native 内存的开销比 Dalvik 堆小,且 GC 不会直接扫描 Native 内存,因此减少了 GC 暂停时间。

无论哪个版本,频繁地申请和释放大块内存(尤其是像素数据)都是性能杀手。BitmapPool 的复用机制可以在任何版本上有效地减少内存分配次数。

🏗️ BitmapPool 的核心接口

Glide 定义了一个清晰的 BitmapPool 接口,主要包含以下几个关键方法:

public interface BitmapPool {
    // 获取一个可复用的 Bitmap,要求宽度、高度、Config 匹配
    Bitmap get(int width, int height, Bitmap.Config config);

    // 将一个不再使用的 Bitmap 放回池中,以备将来复用
    void put(Bitmap bitmap);

    // 根据传入的内存级别(如 TRIM_MEMORY_MODERATE)来清理池中的部分 Bitmap
    void trimMemory(int level);

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

    // 获取池中当前所有 Bitmap 占用的总内存大小(通常以字节为单位)
    long getSize();
}

Glide 默认的实现是 LruBitmapPool,它基于 LRU(Least Recently Used,最近最少使用)算法来管理池中的 Bitmap。

🔍 LruBitmapPool 的实现剖析

LruBitmapPool 内部维护了一个 LruPoolStrategy 策略对象,以及一个记录当前池大小的计数器。LruPoolStrategy 定义了如何查找、添加和移除 Bitmap。Glide 提供了两种策略:

  1. AttributeStrategy:以 Bitmap 的 (宽度、高度、Config) 三元组为键来组织 Bitmap。当请求一个特定尺寸的 Bitmap 时,它只会返回完全匹配的 Bitmap。这种策略的好处是命中即用,无需额外转换,但可能因尺寸细微差别而无法复用。
  2. SizeStrategy:以 Bitmap 的 内存占用大小(字节数) 为键来组织 Bitmap。它允许复用一个足够大的 Bitmap 来容纳新图片,只要新图片所需内存小于等于该 Bitmap 的内存大小,并且 Config 兼容(例如都是 ARGB_8888)。这种策略更灵活,复用率更高,但可能需要将返回的 Bitmap 重新配置(通过 reconfigure 方法),会有一些额外开销。

Glide 4.x 默认使用 SizeStrategy,因为它能最大化复用率。

get(int width, int height, Bitmap.Config config) 的工作流程
  1. 从策略中查找:调用策略的 get(width, height, config) 方法。策略内部会遍历所有可用的 Bitmap,寻找满足条件的对象。
    • 对于 SizeStrategy,它会寻找一个大小大于等于所需字节数,且 Config 兼容的 Bitmap。
  2. 命中处理:如果找到了合适的 Bitmap,会将其从池中移除(因为一旦被取出使用,就不再属于池),然后调用 Bitmapreconfigure(width, height, config) 方法(API 19+)来重置它的尺寸和配置,确保返回的 Bitmap 符合请求的要求。对于 API 19 以下的版本,则不能改变 Bitmap 的尺寸,因此只能返回尺寸完全匹配的 Bitmap。
  3. 未命中处理:如果没有找到合适的 Bitmap,则直接通过 Bitmap.createBitmap(width, height, config) 创建一个新的 Bitmap 返回。
put(Bitmap bitmap) 的工作流程

当一个 Bitmap 不再被使用时(例如从 LruCache 中移除,或者开发者手动释放),Glide 会调用 put 方法将其放回池中。

  1. 检查可用性:首先判断该 Bitmap 是否可以被放入池中。条件是:
    • Bitmap 本身不可变(isMutable)?通常池中的 Bitmap 必须是可变的,因为之后可能会被重新配置。Glide 默认只缓存可变 Bitmap。
    • Bitmap 的尺寸不能太大或太小?如果 Bitmap 的大小超过池的最大容量,则直接丢弃(recycle 或让 GC 回收),因为放进去也很快会被淘汰。
    • Bitmap 的 Config 是否允许被缓存?比如 HARDWARE 类型的 Bitmap(只读,不能修改)通常不能放入池中。
  2. 添加到策略:如果检查通过,就根据策略的规则将 Bitmap 存入内部数据结构。同时更新当前池的总大小。
  3. LRU 淘汰:如果添加后池的总大小超过了预设的 maxSize,就会启动淘汰机制,从池中移除最近最少使用的 Bitmap,直到总大小低于 maxSize。被移除的 Bitmap 如果被丢弃,会调用 recycle() 或者依赖 GC 回收。
引用计数与 BitmapPool 的联动

你可能注意到,BitmapPool 的 put 方法需要开发者主动调用。那么 Glide 是如何知道一个 Bitmap 已经“不再被使用”了呢?这就要提到我们上一轮讲到的 活动缓存(Active Resources)引用计数 机制。

  • 每个正在被显示的图片都对应一个 EngineResource 对象,内部持有一个 Bitmap 和一个引用计数器。
  • 当 ImageView 开始加载图片时,引用计数 +1;当 ImageView 被复用时,引用计数 -1。
  • 当引用计数变为 0 时,表示没有任何视图在使用这张图片了,此时 Glide 会将该 Bitmap 从活动缓存中移除,并调用 bitmapPool.put(bitmap) 将其放回池中,而不是直接销毁。

这样就形成了一个完美的闭环:内存缓存(LruCache) → 活动缓存(引用计数) → BitmapPool → 复用。BitmapPool 作为最底层的内存复用层,将暂时不用的 Bitmap 保存起来,为未来的加载任务提供“二手内存”。

🚀 BitmapPool 带来的好处

  1. 减少 GC 压力:避免了频繁的 Bitmap 对象分配和回收,大幅降低了 GC 的频率和耗时,使滑动列表更加流畅。
  2. 降低内存抖动:内存分配和释放趋于平稳,不会出现忽高忽低的内存峰值。
  3. 提高加载速度:当需要创建一个新的 Bitmap 时,如果能从池中直接获取,就省去了 createBitmap 中的内存分配和像素初始化开销(因为可以直接复用原有像素内存)。
  4. 更可控的内存使用LruBitmapPool 允许设置最大大小,开发者可以根据应用的内存情况合理配置,防止池本身占用过多内存。

⚙️ 如何配置 BitmapPool

Glide 提供了 GlideBuilder 来让我们自定义 BitmapPool 的行为。例如,你可以通过 setBitmapPool 传入自己的实现,或者通过 setMemoryCache 间接影响池的大小(因为内存缓存的大小和 BitmapPool 的大小通常是关联的)。更常见的做法是在 AppGlideModule 中通过 applyOptions 来配置:

@GlideModule
public class MyGlideModule extends AppGlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        // 设置 BitmapPool 的最大内存为 30MB
        builder.setBitmapPool(new LruBitmapPool(30 * 1024 * 1024));
        
        // 或者保持默认,但修改内存缓存的大小(通常 BitmapPool 的大小会随之调整)
        builder.setMemoryCache(new LruResourceCache(20 * 1024 * 1024));
    }
}

📝 总结

BitmapPool 是 Glide 中一个非常经典的内存复用组件,它通过在底层复用 Bitmap 对象的内存空间,极大地优化了图片加载的性能和内存占用。它的设计充分考虑了 Android 不同版本的特性,并结合引用计数机制,确保只有真正不再使用的 Bitmap 才能被回收到池中,从而在 “性能”“正确性” 之间取得了完美的平衡。