资讯详情

资讯详情

Glide 图片加载缓存机制:弱引用与引用队列的设计原理

1. 从一次内存抖动说起为什么要盯着 Glide 的缓存不放做过 Android 图片加载的朋友大概率都遇到过这样的场景列表滑得飞快图片一张接一张地加载突然 Logcat 里开始刷 GC 日志界面肉眼可见地卡了一下然后恢复正常再滑又卡。你打开 Profiler 一看内存曲线像心电图一样上下跳Bitmap 对象频繁创建又回收。这个问题十有八九跟图片缓存的设计有关而 Glide 作为 Android 生态里用得最广的图片加载库它的缓存机制值得每一个 Android 开发者认真拆一遍。Glide 的缓存体系大致分四层活动资源缓存ActiveResources、内存缓存MemoryCache、磁盘缓存DiskCache以及网络或原始数据源。这四层从快到慢、从贵到便宜形成了一个完整的资源获取链路。其中活动资源缓存这一层就是标题里说的弱引用发挥作用的地方。很多人第一次看 Glide 源码的时候会疑惑既然已经有 LruCache 做内存缓存了为什么还要额外搞一个用弱引用存的活动资源缓存这不是多此一举吗这篇文章就是围绕这个问题展开的。我会从 Glide 缓存的分层结构讲起把活动资源缓存的设计动机、弱引用的具体用法、引用队列的配合机制、以及这套设计在实际项目中带来的收益和需要注意的坑一层一层拆开。适合已经用过 Glide、但对内部缓存机制只有模糊印象的 Android 开发者也适合正在准备中高级面试、想把图片加载这块讲清楚的朋友。读完之后你应该能回答出为什么用弱引用这个问题背后的完整逻辑链而不只是背一句防止内存泄漏。2. Glide 四级缓存的分工与数据流向2.1 每一层缓存到底存了什么先把 Glide 的缓存层次理清楚不然后面讲弱引用会缺少参照。Glide 在加载一张图片时会依次经过以下几个查找环节缓存层级存储内容数据结构生命周期活动资源缓存正在被使用的图片资源弱引用 引用队列跟随资源使用状态内存缓存近期使用过的图片资源LruCache跟随应用进程磁盘缓存原始文件或转换后的文件DiskLruCache跟随磁盘文件数据源网络或本地原始数据无一次性活动资源缓存和内存缓存都存在于内存中但它们的职责完全不同。内存缓存用的是 LRU 策略容量有限当新资源进来而容量不够时会按最近最少使用的原则淘汰掉老资源。活动资源缓存则不一样它存的是当前正在被界面使用的资源这些资源随时可能被再次请求所以不能轻易淘汰。这里有个关键点一张图片被加载到 ImageView 上之后它同时存在于活动资源缓存和内存缓存里吗答案是在 Glide 的设计中一个资源要么在活动资源缓存里要么在内存缓存里不会同时存在于两处。当资源被请求使用时它从内存缓存移到活动资源缓存当资源不再被使用时它从活动资源缓存移回内存缓存。这个移动的过程就是理解弱引用设计的关键。2.2 资源在缓存之间的流转过程我用一个具体的加载流程来说明这个流转用户请求加载一张图片Glide 首先查活动资源缓存看有没有正在被使用的同一资源。如果没有查内存缓存LruCache找到了就把资源从内存缓存移除放入活动资源缓存并返回给请求方。如果内存缓存也没有查磁盘缓存找到了就解码成资源放入活动资源缓存。如果磁盘缓存也没有走网络请求拿到数据后解码放入活动资源缓存。当图片不再被任何 ImageView 使用时比如列表滑出了屏幕资源从活动资源缓存移除放回内存缓存。这个流转机制的核心诉求是正在使用的资源不能被回收不在使用的资源要尽快变成可回收状态。活动资源缓存要解决的就是前半句而弱引用在这里扮演了一个非常巧妙的角色。2.3 为什么不能只用 LruCache如果只有 LruCache 一层内存缓存会有什么问题假设一个列表里有 20 张图片正在屏幕上显示它们都在 LruCache 里。这时候用户快速滑动新的图片不断加载进来LruCache 容量满了就会按 LRU 策略淘汰掉一些资源。但问题是那些正在屏幕上显示的图片可能因为最近没有被再次请求而被判定为最少使用从而被淘汰掉。一旦被淘汰如果用户又滑回来就得重新从磁盘或网络加载体验很差。所以 Glide 需要一个机制来标记这些资源正在被使用别动它们。活动资源缓存就是这个标记机制。它把正在使用的资源单独存一份让 LruCache 只负责管理那些暂时不用但可能还会用的资源。这样 LruCache 的淘汰策略就不会误伤正在显示的图片。3. 弱引用在活动资源缓存中的具体用法3.1 弱引用的基本特性回顾在 Java 和 Android 里引用分为四种强引用、软引用、弱引用、虚引用。弱引用的特点是只要发生 GC不管内存是否充足弱引用指向的对象都会被回收。这一点和软引用不同软引用是在内存不足时才回收。Glide 的活动资源缓存用的就是弱引用。具体来说它用一个MapKey, WeakReferenceEngineResource?来存储正在使用的资源。Key 是资源的标识比如图片的 URL、尺寸、变换参数等组合Value 是资源的弱引用。你可能会问用弱引用存那 GC 一来不就把资源回收了吗那还怎么保证正在使用的图片不被回收这个疑问很合理答案在于强引用的持有方。资源被加载到 ImageView 上之后ImageView 本身持有资源的强引用通过 Target 机制所以只要 ImageView 还在显示这张图资源就不会被 GC 回收。活动资源缓存里的弱引用只是一个索引用来快速查找而不是资源的唯一持有者。3.2 引用队列如何配合弱引用工作光有弱引用还不够因为当资源被 GC 回收后Map 里会残留一个指向 null 的 WeakReference 条目。如果不清理这个 Map 会越来越大造成内存泄漏。Glide 用**引用队列ReferenceQueue**来解决这个问题。具体做法是创建 WeakReference 的时候把 ReferenceQueue 传进去。当弱引用指向的对象被 GC 回收时这个 WeakReference 本身会被加入到 ReferenceQueue 中。Glide 在每次操作活动资源缓存时会先检查 ReferenceQueue 里有没有待清理的引用有的话就把对应的条目从 Map 里移除。// 简化后的逻辑示意 ReferenceQueueEngineResource? queue new ReferenceQueue(); // 存入时 WeakReferenceEngineResource? weakRef new WeakReference(resource, queue); activeResources.put(key, weakRef); // 清理时 Reference? extends EngineResource? ref; while ((ref queue.poll()) ! null) { // 根据 ref 找到对应的 key从 map 中移除 activeResources.values().remove(ref); }这个机制保证了 Map 里不会残留无效的弱引用条目。实际源码中Glide 会把 Key 和 WeakReference 包装成一个 Entry 对象Entry 继承自 WeakReference这样从队列里取出的 ref 可以直接强转成 Entry拿到 Key 做清理。3.3 资源引用计数的配合除了弱引用和引用队列活动资源缓存还有一个重要的配合机制引用计数。EngineResource 内部有一个acquired变量记录当前有多少个地方在使用这个资源。每次资源被请求计数加一每次资源被释放计数减一。当计数归零时资源才会从活动资源缓存移回内存缓存。这个设计解决了一个问题同一张图片可能同时被多个 ImageView 使用比如聊天界面里同一张表情包出现多次。如果只靠弱引用无法知道资源是否还在被使用。引用计数提供了精确的使用状态跟踪和弱引用配合形成了完整的资源生命周期管理。注意引用计数归零并不等于资源立即被回收它只是从活动资源缓存移到内存缓存变成可被 LRU 淘汰的状态。真正回收要等 LruCache 淘汰或进程结束。4. 弱引用方案背后的设计权衡4.1 为什么不用软引用或强引用这是一个很自然的对比问题。软引用的特点是内存不足时才回收看起来比弱引用更安全那为什么 Glide 不用软引用原因在于回收时机的不可控性。软引用在内存不足时被回收但内存不足这个时机是由 GC 决定的开发者无法精确控制。在图片加载场景下如果大量资源用软引用持有可能导致内存一直处于较高水位直到触发 GC 才集中回收造成卡顿。而弱引用的回收更及时配合引用计数和引用队列可以做到资源的精细化管理。强引用就更不行了强引用持有资源意味着资源永远不会被回收除非手动移除。在列表快速滑动的场景下大量图片资源被强引用持有很快就会 OOM。所以弱引用的选择本质上是在及时回收和使用中不被回收之间找平衡。使用中的资源由 ImageView 强引用持有不会因为弱引用被回收不在使用的资源弱引用允许 GC 及时回收避免内存堆积。4.2 弱引用带来的一个潜在问题弱引用方案并非完美。有一个容易被忽略的问题如果资源只被活动资源缓存弱引用持有而没有任何其他强引用那么它随时可能被 GC 回收。在 Glide 的设计中这种情况理论上不应该发生因为资源被请求后会被 Target 强引用。但如果代码里出现了资源请求了但没有正确绑定到 Target 的情况就可能出现资源被提前回收导致加载失败或闪烁。这也是为什么 Glide 要求所有资源请求都必须通过into()方法绑定到 Target。如果你绕过 Target 直接拿资源就要自己保证强引用。这个约束在自定义 Target 或做预加载时需要特别注意。4.3 和 LruCache 的容量配合活动资源缓存本身没有容量限制因为它存的是正在使用的资源数量理论上受限于屏幕能显示的内容。但实际上如果代码里有资源泄漏比如 Target 没有正确释放活动资源缓存里的条目会异常增多。虽然弱引用允许 GC 回收但引用队列的清理是下次操作时才触发的如果长时间不操作缓存Map 里的无效条目会暂时堆积。所以实际使用中如果发现内存异常除了看 LruCache 的命中率和大小也要关注活动资源缓存的条目数。可以通过自定义 GlideModule 或者用 Profiler 观察activeResources这个 Map 的大小变化。5. 从源码角度看弱引用的实现细节5.1 ActiveResources 类的核心结构Glide 中负责活动资源缓存的类是ActiveResources。它的核心字段包括一个MapKey, ResourceWeakReference类型的activeResources用 HashMap 实现。一个ReferenceQueueEngineResource?类型的resourceReferenceQueue。一个Handler或Executor用于在主线程处理引用队列的清理。ResourceWeakReference继承自WeakReferenceEngineResource?同时持有 Key 和一个isCacheable标志。这个标志用来区分资源是否可以被缓存有些资源比如某些变换后的临时资源可能不需要进入内存缓存。5.2 activate 和 deactivate 的完整流程资源的激活和去激活是活动资源缓存的两个核心操作activate 流程资源被请求使用从内存缓存LruCache中移除该资源。创建 ResourceWeakReference传入引用队列。将弱引用放入 activeResources Map。资源引用计数加一。deactivate 流程资源不再被使用资源引用计数减一。如果计数归零从 activeResources Map 中移除。如果资源可缓存放入内存缓存LruCache。如果不可缓存直接回收。这两个流程保证了资源在使用中和空闲两种状态之间的正确切换。弱引用在 activate 时创建在 deactivate 时移除生命周期和资源的使用状态严格对应。5.3 清理时机与线程模型引用队列的清理时机很关键。Glide 在每次调用get()、activate()、deactivate()等操作时都会先调用一次清理逻辑检查引用队列里有没有待处理的引用。这种懒清理的方式避免了额外的线程开销但也意味着如果长时间不操作缓存无效条目会暂时存在。清理操作本身是在主线程执行的因为 activeResources Map 不是线程安全的。Glide 通过确保所有对活动资源缓存的访问都在主线程来避免并发问题。这一点在自定义 Glide 组件时需要注意不要在其他线程直接操作这个 Map。// 清理逻辑的简化示意 private void cleanupActiveReference(ResourceWeakReference ref) { synchronized (this) { activeResources.remove(ref.key); } }提示如果你在做 Glide 的性能优化可以关注cleanupActiveReference的调用频率。频繁的清理操作虽然单次开销小但在高频加载场景下也会累积成可观的耗时。6. 实际项目中与弱引用缓存相关的坑6.1 资源闪烁与弱引用提前回收前面提到过如果资源没有被正确强引用弱引用可能被提前回收。实际项目中这个问题最常见的表现是图片加载完成后短暂显示然后突然变成占位图或空白。排查思路是检查 Target 的生命周期管理确认into()绑定的 Target 是否在资源显示期间一直存活。有一种情况容易踩坑在 RecyclerView 的onBindViewHolder里手动创建 Target 并加载图片但没有在onViewRecycled里正确清理。这样当 Item 被回收时Target 可能被置空导致资源失去强引用被 GC 回收后图片消失。6.2 内存缓存命中率异常如果发现内存缓存命中率很低除了检查 LruCache 的配置还要看活动资源缓存是否正常工作。有一种情况是资源被频繁地 activate 和 deactivate导致它在活动资源缓存和内存缓存之间反复移动每次移动都涉及 Map 操作和可能的清理开销不小。这种情况通常出现在列表滚动时图片频繁进出屏幕。Glide 本身对这种情况有优化但如果自定义了skipMemoryCache或者用了diskCacheStrategy不当可能加剧这个问题。建议在列表场景下保持默认的内存缓存策略让 Glide 自己管理资源的流转。6.3 弱引用与 Bitmap 池的配合Android 的 Bitmap 在 3.0 之后把像素数据放在 native 堆Java 堆里只有一个小对象。弱引用回收的是 Java 对象但 native 内存的释放依赖于 Bitmap 的recycle()或 GC 的 finalizer。这意味着即使弱引用被回收native 内存可能不会立即释放。Glide 通过 BitmapPool 来复用 Bitmap减少 native 内存的分配和释放。活动资源缓存和 BitmapPool 的配合是资源被回收时如果 Bitmap 可复用会放回 BitmapPool下次需要时从池里取避免重新分配。这个机制和弱引用是互补的弱引用管 Java 对象的生命周期BitmapPool 管 native 内存的复用。问题现象可能原因排查方向图片显示后消失资源被提前回收检查 Target 生命周期内存缓存命中率低资源频繁流转检查缓存策略配置native 内存不释放Bitmap 未复用检查 BitmapPool 配置活动资源缓存条目异常多资源泄漏检查 Target 释放逻辑7. 自定义缓存策略时的注意事项7.1 什么时候需要动活动资源缓存大多数情况下不需要自定义活动资源缓存。Glide 的默认实现已经覆盖了绝大多数场景。但有两种情况可能需要关注第一种是超高频加载场景比如短视频应用的封面图列表图片切换极快。这时候活动资源缓存的 activate/deactivate 频率很高可以考虑调整内存缓存大小或使用自定义的缓存策略来减少流转开销。第二种是大图预览场景比如图片查看器单张图很大占用内存多。这时候要确保大图不被 LruCache 长期持有可以通过skipMemoryCache(true)让大图只走活动资源缓存和磁盘缓存减少内存压力。7.2 自定义 Target 时的强引用管理如果自定义 Target必须保证在资源使用期间持有强引用。常见的做法是在 Target 的onResourceReady里把资源保存到成员变量在onLoadCleared里置空。这样只要 Target 存活资源就不会被弱引用机制影响。public class CustomTarget extends CustomViewTargetImageView, Drawable { private Drawable currentResource; Override public void onResourceReady(Drawable resource, Transition? super Drawable transition) { currentResource resource; // 强引用 view.setImageDrawable(resource); } Override public void onLoadCleared(Drawable placeholder) { currentResource null; // 释放强引用 view.setImageDrawable(placeholder); } }这个模式看起来简单但实际写的时候容易漏掉onLoadCleared的清理导致资源被 Target 长期强引用反而造成内存泄漏。所以强引用的管理是双向的既要保证使用期间有强引用也要保证释放后及时断开。7.3 监控与调优建议如果你想验证活动资源缓存的工作状态可以通过以下方式用 Android Studio 的 Memory Profiler 观察ActiveResources实例的 retained size 和对象数量。在 Debug 模式下打印activeResourcesMap 的大小观察列表滚动时的变化。关注 GC 日志中 WeakReference 的回收频率如果异常频繁说明资源流转过快。调优的核心思路是让正在使用的资源稳定停留在活动资源缓存让不用的资源尽快回到内存缓存或释放。如果发现资源在两层缓存之间频繁移动就要检查是不是有代码在反复请求和释放同一资源。8. 弱引用机制给我们的通用启示Glide 用弱引用做活动资源缓存这个设计思路其实可以迁移到很多其他场景。核心思想是用弱引用做索引用强引用做持有用引用队列做清理。这三者配合既能快速查找资源又能保证资源不被误回收还能避免内存泄漏。我在做其他缓存系统的时候也借鉴过这个模式。比如做一个图片编辑器的图层缓存正在编辑的图层用强引用持有缓存索引用弱引用图层关闭时通过引用队列清理索引。效果比单纯用 LruCache 好很多因为 LruCache 的淘汰策略在正在使用这个维度上是盲的。另一个启示是引用计数和弱引用的配合。弱引用解决对象是否还存在的问题引用计数解决对象是否还在被使用的问题两者结合才能完整描述资源的生命周期。单独用任何一个都有缺陷只用弱引用无法知道资源是否还在使用只用引用计数无法感知资源被 GC 回收的情况。最后说一个实际体会理解 Glide 的缓存机制最好的方式不是死记源码而是自己动手写一个简化版的图片加载器把活动资源缓存、LruCache、引用队列这几个组件串起来。写一遍之后再回头看 Glide 的源码很多设计决策就自然明白了。我在带新人的时候通常会让他们先实现一个只支持内存缓存和磁盘缓存的版本然后再加入活动资源缓存和弱引用对比前后的内存表现和加载速度效果比单纯讲原理好得多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →