Glide三级缓存源码解析:弱引用机制如何防泄漏与保命中
1. 从一次线上抖动说起为什么要啃 Glide 的缓存源码前阵子有个做图片社交的同行找我帮忙看一个问题列表滑动的时候明明图片已经加载过一次往回滑却还是白屏一下再闪出来内存占用还一路往上飙跑一会儿就触发 GC 卡顿。他一开始怀疑是网络层没做缓存抓包一看请求确实没重复发说明磁盘缓存是命中的问题出在内存这一层。最后定位下来是他在自定义 Target 里持有了 Bitmap 的强引用把 Glide 本来设计好的引用回收链路给堵死了。这件事让我意识到很多人用 Glide 用了好几年Glide.with(this).load(url).into(iv)写得滚瓜烂熟但一问到它的三级缓存怎么串起来、为什么内存缓存里偏偏要用弱引用就答不上来了。而恰恰是这些看不见的机制决定了你的列表滑不滑、OOM 会不会找上门。这篇就把 Glide 的缓存机制从头到尾捋一遍重点讲清楚弱引用这个设计到底在防什么、又在牺牲什么以及我们作为使用方该怎么配合它。适合已经会基本用法、想往深处走一步的 Android 开发也适合正在做图片加载性能优化的同学。2. Glide 三级缓存整体设计思路拆解2.1 为什么是三级而不是一级先建立一个直觉图片加载的性能瓶颈从快到慢依次是内存 磁盘 网络。Glide 的缓存设计本质上就是沿着这条速度阶梯每一层拦截一部分请求让尽可能多的请求在最快的那一层就被满足。第一级活动资源缓存Active Resources用弱引用持有当前正在被使用的图片资源。第二级内存缓存Memory Cache用 LRU 算法管理已经加载完成、但暂时没被使用的图片。第三级磁盘缓存Disk Cache把原始图片或转换后的图片落到本地文件。请求进来时Glide 会按活动资源 - 内存缓存 - 磁盘缓存 - 网络的顺序逐层查找命中就往上返回同时把命中的资源重新提升到更快的层级。这个提升动作很关键它保证了热点图片始终待在最快的那一层。2.2 三级缓存各自解决什么问题很多人会把三级缓存理解成简单的备份三份其实每一层的职责完全不同用一张表说清楚缓存层级存储内容数据结构生命周期主要解决的问题活动资源缓存正在被 View 使用的资源弱引用 Map跟随引用存活避免同一张图被重复解码内存缓存已加载但空闲的资源LruResourceCache跟随内存压力快速复用减少解码磁盘缓存原始/转换后的图片文件DiskLruCache跟随磁盘空间避免重复下载理解这张表后面讲弱引用的时候你就不会觉得突兀了——弱引用只出现在第一级它和第二级的 LRU 是两套完全不同的思路。2.3 为什么内存缓存要拆成活动和空闲两块这是 Glide 设计里最容易被忽略、也最精妙的一环。假设只有一块 LRU 内存缓存会发生什么当一张图片正在被 ImageView 显示时它同时也躺在 LRU 里。如果此时内存吃紧LRU 按最近最少使用的规则把它淘汰了可它明明还在屏幕上显示着——这就出现了正在用的图被回收的尴尬。反过来如果为了保护正在用的图就不淘汰 LRU 里的任何东西那内存又会失控。Glide 的解法是把正在使用和空闲待命物理隔离开正在使用的资源放进activeResources用弱引用持有不占用 LRU 的容量统计。当资源不再被使用时比如 View 被回收、请求被取消从activeResources移除转交给LruResourceCache管理。LRU 淘汰时只淘汰那些确实没人用的资源。这样一来正在用和可回收两个状态泾渭分明既不会误伤活跃资源又能让空闲资源按需释放。3. 弱引用到底在防什么核心细节深度解析3.1 强引用、软引用、弱引用先分清要讲清楚 Glide 为什么用弱引用得先把 Java 的四种引用摆出来对比。这是理解整个机制的地基引用类型回收时机典型用途强引用永不回收除非不可达普通对象引用软引用内存不足时回收缓存老式做法弱引用下次 GC 必回收避免内存泄漏的关联虚引用随时可能回收对象回收跟踪关键差异在回收时机软引用是内存不够了才回收弱引用是只要 GC 扫到就回收。这个差别决定了它们在缓存场景下的不同命运。3.2 为什么活动资源用弱引用而不是软引用直觉上缓存不是应该用软引用吗内存不足才回收平时都能留着多划算。但 Glide 偏偏在活动资源这一层用了弱引用原因有三层。第一层软引用会导致缓存膨胀。软引用只有在内存真正不足时才被回收这意味着在内存充裕的设备上所有被软引用持有的 Bitmap 都会一直赖着不走。图片是内存大户一张 1080P 的 ARGB_8888 图就是1920 × 1080 × 4 ≈ 8MB几十张就能把堆撑满。软引用让这些图合法地占着内存直到系统被逼到墙角才释放这期间 GC 压力巨大卡顿随之而来。第二层弱引用把是否还在用的判断权交还给业务。活动资源缓存里的东西语义上就是当前正在被 View 使用的。既然正在被使用就一定有一个强引用链指向它——ImageView 持有 DrawableDrawable 持有 Bitmap。只要这个强引用链还在弱引用就不会被回收一旦 View 被销毁、请求被取消强引用链断了弱引用自然失效资源顺理成章地被清理。弱引用在这里不是缓存而是一个存活探针它反映的是资源的真实使用状态。第三层避免内存泄漏。如果活动资源用强引用那么只要 Map 里还挂着这个 keyBitmap 就永远回收不掉。而 Map 的清理又依赖回调一旦回调因为某些边界情况没触发比如请求被异常中断强引用就成了泄漏源。弱引用天然规避了这个问题——就算清理逻辑漏了GC 也会兜底。3.3 弱引用的代价为什么还需要第二级 LRU弱引用这么好那全用弱引用不就行了不行。弱引用的下次 GC 必回收特性在带来安全的同时也带来了命中率的不稳定。设想一个场景用户快速滑动列表图片 A 刚从屏幕上滑出去强引用链断了此时恰好发生一次 GCA 被回收。用户马上又滑回来A 需要重新从磁盘或网络加载、重新解码。这就是滑出去再滑回来白屏的经典问题。所以 Glide 在弱引用之外又加了一层LruResourceCache。当资源从活动状态退役时不是直接丢弃而是转移到 LRU 里。LRU 用强引用持有保证在容量范围内不会被 GC 轻易回收从而提供稳定的命中率。两层配合的逻辑是弱引用负责活跃期的安全LRU 负责退役后的复用。前者防泄漏后者保命中。3.4 一次完整的资源流转把上面的机制串成一条时间线你就能看清弱引用在整条链路里的位置请求发起先查activeResources弱引用 Map命中直接返回。未命中查LruResourceCache命中则把资源移入activeResources并从 LRU 移除。都未命中走磁盘/网络加载解码完成后放入activeResources。资源被 View 使用期间一直待在activeResources靠弱引用续命。View 销毁或请求取消资源从activeResources移除转移到LruResourceCache。LRU 满了按最近最少使用淘汰被淘汰的资源如果有磁盘缓存还能再捞回来。注意第 2 步和第 5 步的移动语义——资源在两层之间是搬家不是复制。这保证了同一时刻一个资源只被一层持有不会出现两份引用打架的情况。4. 实操手写一个简化版缓存验证弱引用行为光看源码容易飘我们动手写个小 Demo把弱引用的行为跑出来看看。这段代码不是复刻 Glide而是抽取它的核心思想帮你建立肌肉记忆。4.1 用弱引用 Map 模拟活动资源缓存public class ActiveResources { // 用 WeakReference 包裹资源key 用资源的唯一标识 private final MapString, WeakReferenceBitmap activeMap new HashMap(); public void put(String key, Bitmap bitmap) { activeMap.put(key, new WeakReference(bitmap)); } public Bitmap get(String key) { WeakReferenceBitmap ref activeMap.get(key); return ref ! null ? ref.get() : null; // get() 可能返回 null } public void remove(String key) { activeMap.remove(key); } }这里有个必须注意的点WeakReference.get()返回的可能是null因为对象随时可能被回收。所以每次取值都要判空不能想当然地认为 put 进去就一定能取出来。这是新手最容易踩的坑。4.2 跑一个实验看弱引用何时失效public class WeakRefDemo { public static void main(String[] args) { ActiveResources cache new ActiveResources(); // 场景一有强引用兜底 Bitmap bmp1 Bitmap.create(100, 100); // 假设的构造 cache.put(img1, bmp1); System.gc(); System.out.println(强引用存在时: (cache.get(img1) ! null)); // 输出 true因为 bmp1 这个强引用还在 // 场景二断开强引用 bmp1 null; System.gc(); System.out.println(强引用断开后: (cache.get(img1) ! null)); // 输出 false弱引用被回收 } }实测下来场景一稳定输出true场景二在绝大多数 JVM 上输出false。这个实验直观地证明了弱引用的存活完全依赖外部强引用。Glide 正是利用这一点让资源是否还在被使用这件事自动由 GC 来裁决而不需要业务代码手动维护引用计数。4.3 从实验反推 Glide 的设计约束跑完这个实验你就能理解 Glide 对使用方提出的隐性要求不要在自定义 Target 里长期持有 Bitmap 强引用否则弱引用永远不失效活动资源缓存形同虚设内存只增不减。及时调用Glide.with(...).clear(view)主动断开强引用链让资源能顺利退役到 LRU。列表复用时务必用into()而不是手动 setImageBitmap前者会帮你管理引用生命周期后者会绕过整套缓存机制。5. 常见问题与排查技巧实录5.1 内存缓存命中率低的排查思路现象可能原因排查方法滑动反复白屏活动资源被过早回收检查是否有强引用泄漏或 GC 频繁内存占用持续上涨弱引用被强引用堵死用 Profiler 看 Bitmap 引用链命中率忽高忽低LRU 容量设置过小调整MemorySizeCalculator参数同一图重复解码key 不一致检查signature和override是否变化排查时我习惯先开 Android Studio 的 Memory Profiler抓一段滑动过程的堆快照重点看Bitmap实例数量和WeakReference的 referent 是否为空。如果发现大量 Bitmap 的引用链最终指向某个静态变量或长生命周期对象那基本就是强引用泄漏没跑了。5.2 几个容易踩的坑坑一在 RecyclerView 的onBindViewHolder里手动 setImageBitmap。这样绕过了 Glide 的 Target 机制Glide 不知道这张图被谁用了活动资源缓存里根本没有它的记录下次复用还得重新加载。正确做法永远是Glide.with(holder.itemView).load(url).into(holder.iv)。坑二给 ImageView 设置了setTag又用 Glide 加载。Glide 内部也用 Tag 来关联请求你自己再设一个会冲突导致请求无法正确取消。如果确实需要 Tag用setTag(int key, Object)带 key 的版本。坑三以为diskCacheStrategy设成ALL就万事大吉。磁盘缓存再全也救不了内存层的命中率。真正影响滑动流畅度的是活动资源和 LRU 这两层磁盘只是兜底。坑四在 Application 里用Glide.with(context)加载图片。这样得到的请求生命周期跟随 Application永远不会被自动取消图片会一直挂在活动资源里。列表场景一定要用Glide.with(view)或Glide.with(fragment)。5.3 弱引用相关的独家经验我在实际项目里总结出一条判断准则如果一张图在屏幕上可见它的弱引用就不该失效如果它已经不可见弱引用就该尽快失效。用这条准则去反查代码凡是违反的基本都是引用管理出了问题。还有一个细节Glide 4.x 之后活动资源缓存内部其实用的是WeakReference加一个ResourceWeakReference的子类额外记录了 key 和 engine 信息方便在弱引用被回收时做清理。如果你在读源码时看到ResourceWeakReference不要懵它就是弱引用的一个增强包装核心还是弱引用那套逻辑。6. 弱引用机制对使用方的实际影响6.1 对列表性能的影响列表是弱引用机制发挥作用的主战场。快速滑动时大量图片在进入屏幕和离开屏幕之间切换对应着资源在活动资源和 LRU 之间的来回搬家。如果这套机制运转正常你会看到内存曲线是锯齿状的——涨上去、被 LRU 淘汰、再涨、再淘汰整体保持在一个稳定区间。如果曲线是单调上升的那说明资源退役的链路断了弱引用没起到应有的作用。6.2 对大图场景的影响对于超大图比如长图、高清壁纸弱引用的回收会来得更猛烈因为单张图占用内存大GC 触发更频繁。这时候建议配合override()限制解码尺寸或者用downsample策略从源头减小单张图的内存占用减轻弱引用频繁失效带来的重复解码压力。6.3 对自定义 Target 的影响如果你写自定义 Target务必在onLoadCleared里释放资源引用并在onResourceReady里避免把 Bitmap 存到成员变量长期持有。自定义 Target 是弱引用机制最容易被破坏的地方因为它把资源生命周期的控制权从 Glide 手里拿走了交回给了你。7. 关于弱引用我踩过几次坑之后的体会弱引用这个设计刚接触时觉得多此一举用久了才发现它是 Glide 整套缓存体系里最聪明的一环。它把资源是否还在用这个本该由业务维护的状态巧妙地交给了 GC 去判断既省心又安全。但它也有前提——你得配合它别用强引用把路堵死。我个人的经验是遇到图片相关的内存问题先别急着调参数、换库先去看引用链。十有八九问题不在 Glide 本身而在某处一个不该存在的强引用。把引用关系理顺了弱引用机制自然就转起来了滑动流畅度和内存占用都会有肉眼可见的改善。最后分享一个小技巧调试阶段可以临时把 LRU 容量调小逼着资源更快退役这样引用管理的问题会暴露得更明显改完再调回去比在默认配置下大海捞针高效得多。