Android Glide 笔记

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

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

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

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

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

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

flowchart TD A[开始加载图片] --> B{活动缓存
ActiveResources}; B -- 命中 --> C[直接返回图片
并更新引用计数]; B -- 未命中 --> D{内存缓存
LruResourceCache}; D -- 命中 --> E[将图片从内存缓存
移入活动缓存]; E --> C; D -- 未命中 --> F{磁盘缓存
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 接口,主要包含以下几个关键方法:

java 复制代码
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 来配置:

java 复制代码
@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 才能被回收到池中,从而在 "性能""正确性" 之间取得了完美的平衡。

相关推荐
城东米粉儿2 小时前
Android TheRouter 笔记
android
城东米粉儿8 小时前
Android AIDL 笔记
android
城东米粉儿8 小时前
Android 进程间传递大数据 笔记
android
城东米粉儿8 小时前
Android KMP 笔记
android
冬奇Lab10 小时前
WMS核心机制:窗口管理与层级控制深度解析
android·源码阅读
松仔log10 小时前
JetPack——Paging
android·rxjava
城东米粉儿11 小时前
Android Kotlin DSL 笔记
android
城东米粉儿11 小时前
Android Gradle 笔记
android