OkHttp 响应缓存深度指南:Cache 构建、EventListener 事件监控、修剪与排障 OkHttp 响应缓存深度指南Cache 构建、EventListener 事件监控、修剪与排障【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttpOkHttp 提供了一个可选的磁盘响应缓存Cache默认关闭目标是做到 RFC 正确且务实在语义模糊时参照 Firefox/Chrome 等真实浏览器的行为。本文基于官方文档 caching.md 与对应源码覆盖如何为OkHttpClient挂载 50 MiB 磁盘缓存、如何通过EventListener观察 CacheHit / CacheMiss / ConditionalHit 三类事件、如何删除或修剪缓存条目、以及“响应明明可缓存却没进缓存”这类问题的源码级排查路径。基础用法为 OkHttpClient 挂载磁盘缓存在OkHttpClient.Builder上调用cache(...)即可启用缓存。文档给出的典型配置是 50 MiB 的磁盘缓存注释里提到这在 2020 年相当于 0.05 美元的手机存储成本private val client: OkHttpClient OkHttpClient.Builder() .cache(Cache( directory File(application.cacheDir, http_cache), // $0.05 worth of phone storage in 2020 maxSize 50L * 1024L * 1024L // 50 MiB )) .build()对应到仓库源码Cache.kt 中面向普通 JVM/Android 场景的构造器就是constructor(directory: File, maxSize: Long)L167-L171它委托给 okio 的FileSystem.SYSTEM并使用 DiskLruCache 做底层的 LRU 淘汰L173-L181。几个对实战有影响的实现细节缓存键是 URL 的 MD5key(url)直接把 URL 字符串做 MD5 十六进制摘要L746-L752所以 URL 中查询串不同即视为不同条目。默认只缓存 GET 响应Cache.put()中对非 GET 方法直接返回 null注释说明虽然规范上允许缓存 HEAD、QUERY 和部分 POST但“复杂度高、收益低”L231-L235。Vary: *的响应不可缓存response.hasVaryAll()为 true 时直接放弃写入L237-L239。读取时校验 Varyget()取到快照后调用Entry.matches()逐字段比较 Vary 请求头不匹配则丢弃L210-L214。HTTPS 响应会连同 TLS 握手信息加密套件、证书链、TLS 版本一起写入条目格式细节见Entry类的 KDocL476-L526因此response.handshake可以来自缓存。一个可直接运行的仓库示例是 CacheResponse.java它创建 10 MiB 缓存、对同一 URL 请求两次并打印每次的cacheResponse/networkResponse以观察第二次请求命中缓存。EventListener 事件看懂三类缓存路径缓存行为通过EventListenerAPI 暴露EventListener.kt 中定义了三个 open 回调cacheHitL454、cacheMissL466、cacheConditionalHitL476。文档归纳了三类典型场景1. Cache Hit完全命中最理想的情况缓存直接满足请求不发起任何条件请求因此跳过 DNS、建连、下载响应体等常规事件。CallStart → CacheHit → CallEnd2. Cache Miss未命中请求走完整网络路径额外多出一个CacheMiss事件表明缓存的存在CallStart → CacheMiss → ProxySelectStart → … Standard Events … → CallEnd未命中通常发生在条目从未从网络读取过、响应不可缓存、或依据响应缓存头已过期。3. Conditional Cache Hit条件命中当缓存条目需要向服务器校验是否仍有效时先收到cacheConditionalHit事件随后发生一次真实的网络往返若服务器返回 304则响应体为 0 字节最终仍记一次 CacheHitCallStart → CacheConditionalHit → ConnectionAcquired → … Standard Events … → ResponseBodyEnd (0 bytes) → CacheHit → ConnectionReleased → CallEnd此时的Response中cacheResponse与networkResponse均非 null仅当网络响应码为HTTP/1.1 304 Not Modified时顶层响应才会采用缓存副本合并双方头部。这些事件与分支逻辑都能在 CacheInterceptor.kt 中一一对应networkRequest null无需网络→ 触发cacheHitL80-L88有缓存候选但需要网络 →cacheConditionalHit否则 →cacheMissL90-L94网络响应为 304 时按 RFC 7234 §4.3.4 合并缓存头与网络头combine()L240-L269调用cache.update()刷新条目后再触发cacheHitL107-L127。启发式新鲜期。文档还指出按 HTTP RFC 建议响应的默认最大新鲜期取文档“年龄”基于Last-Modified的 10%且含查询串的 URL 不使用默认过期。CacheStrategy.kt 的computeFreshnessLifetime()精确实现了这一点仅在url.query null时用servedMillis - lastModified除以 10 作为新鲜期L250-L257。这意味着GET /api/items?id5这类带 query 的 URL 若服务器未下发Cache-Control/Expires将永远走条件请求或 miss。缓存目录独占、删除与惰性初始化文档强调缓存目录必须由单个 Cache 实例独占拥有内部数据结构可能因共享而损坏但同一个Cache实例可以被多个OkHttpClient共用见 Cache.kt 类 KDocL54-L60。cache.delete()delete()会关闭缓存并删除目录内所有文件——包括不是由缓存创建的文件KDocL298-L305。官方提醒这会让缓存失去“跨应用重启持久化”的意义应谨慎。缓存默认惰性初始化首次使用时读取 journal、重建内存索引也可以在后台线程提前调用cache.initialize()完成初始化避免首次请求抖动KDocL282-L296。其他常用观测 APIsize()当前占用字节、maxSize()、flush()、close()以及三个命中率统计requestCount()/hitCount()/networkCount()L419-L423。注意统计口径条件命中会同时计入 network count 和 hit counttrackConditionalCacheHit()L415-L417。修剪缓存evictAll 与 urls() 迭代器临时清理全部空间cache.evictAll()evictAll()删除所有已存值进行中的写入会正常完成但对应响应不会被保留KDocL307-L314。删除单个条目则使用urls()迭代器——典型场景是用户执行“下拉刷新”这类强制刷新操作后清除指定域名的缓存val urlIterator cache.urls() while (urlIterator.hasNext()) { if (urlIterator.next().startsWith(https://www.google.com/)) { urlIterator.remove() } }从 Cache.kt 的实现看urls()返回一个MutableIteratorString它逐个读取磁盘条目的元数据首行即 URLremove()委托给底层DiskLruCache的 snapshot 迭代器把对应条目驱逐。KDoc 还说明两点行为约束迭代过程中新增的响应不会出现在结果中迭代中被驱逐的既有条目会缺席。迭代器不会抛ConcurrentModificationException。排障可缓存的响应为什么没被缓存文档给出的第一条排查要点必须把响应读完整。未读完、被取消或中途卡住的响应不会写入缓存。这一行为的实现链路值得理解CacheInterceptor.kt 在确认响应可缓存后调用cacheWritingResponse()L143-L152把响应体包装成一个“边读边写”的 source读到底读到 -1关闭cacheBodyRealCacheRequest内部的editor.commit()才执行条目正式落盘L183-L214commit 在 Cache.kt 的RealCacheRequest.body().close()中提前关闭且未能读尽尝试在超时内 discard 剩余字节失败则cacheRequest.abort()丢弃部分条目L219-L227读取过程抛 IOException立即 abortL195-L201。此外put()本身会拒绝非 GET 响应、Vary: *响应、缓存不可写IOException的响应L219-L251。因此排查顺序是先确认response.body被完整消费如.string()/exhausted()再检查响应方法、Vary 头与Cache-Control指令最后用writeSuccessCount()/writeAbortCount()L367-L369观察写入成败比例。覆盖默认缓存行为CacheControl 指令文档把“覆盖默认缓存行为”指向Cache类文档而 Cache.kt 的 KDocL81-L143给出了完整指令集目的指令说明强制走网络全量刷新noCache跳过缓存直接请求服务器仅强制重新校验maxAge(0, TimeUnit.SECONDS)更省流量允许 304 短路仅用缓存onlyIfCached缓存不足时返回 504允许使用过期响应maxStale(N, unit)缓存条目过期 N 秒内仍可使用// 强制走网络 val forceNetwork Request.Builder() .cacheControl(CacheControl.Builder().noCache().build()) .url(http://publicobject.com/helloworld.txt) .build() // 强制只用缓存 val onlyCache Request.Builder() .cacheControl(CacheControl.Builder().onlyIfCached().build()) .url(http://publicobject.com/helloworld.txt) .build()两个可落地的源码佐证onlyIfCached请求若缓存不可用CacheInterceptor 会直接构造一个504 Gateway Timeoutmessage 为 Unsatisfiable Request (only-if-cached)并触发satisfactionFailure事件——所以示例代码中判断code ! 504才算命中缓存。CacheControl.kt 提供了现成常量FORCE_NETWORKL268与FORCE_CACHEL276分别对应上述两种“强制”场景无需手动拼 Builder。小结结合 caching.md 与源码可以把 OkHttp 缓存的关键结论收敛为Cache(File, maxSize)一行挂载、URL 的 MD5 做键、只缓存 GET、Vary: *不可缓存事件链路由cacheHit/cacheMiss/cacheConditionalHit完整刻画新鲜期计算遵循max-age→Expires→Last-Modified10% 启发式的优先级query URL 除外条目必须被完整读取才会 commit 落盘运维侧用delete()、evictAll()、urls()三个 API 覆盖“全清 / 单删”场景。相关源码入口依次为 Cache.kt、CacheInterceptor.kt、CacheStrategy.kt 与 EventListener.kt可继续深入阅读。【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考