OkHttp响应缓存教程:RFC 9111标准缓存、条件缓存命中如何帮你省流量
【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp
OkHttp 是面向 JVM、Android 和 GraalVM 的 HTTP 客户端,它的响应缓存严格遵循 RFC 9111 标准,能把重复请求直接由本地缓存命中,彻底省掉一次网络往返。本教程带你搞懂:如何开启 OkHttp 缓存、三种缓存命中场景的区别,以及条件缓存(ETag / 304)为什么是省流量的关键。
如上图所示,OkHttp 的每一个请求都会先经过核心的缓存判断(CACHE),再决定是否真正走网络(NETWORK)——这就是省流量的第一步。
为什么要用 OkHttp 响应缓存?
没有缓存时,每次请求都要经历 DNS 解析、建连、TLS 握手、下载响应体,耗时会高达几百毫秒。启用缓存后:
- 缓存命中(Cache Hit):完全不访问网络,响应速度从几百毫秒降到几毫秒,流量消耗为 0;
- 条件命中(Conditional Hit):只传一个小小的校验头,服务器返回
304 Not Modified时响应体为 0 字节,流量几乎为零; - 离线可用:弱网或无网场景下,
only-if-cached模式可直接读取本地副本。
OkHttp 的缓存默认关闭,需要显式启用,且目标是"RFC 正确 + 务实"的行为,遇到规范歧义时与 Chrome/Firefox 等现代浏览器保持一致。
一键开启缓存:最简单配置方法
只需要在构建OkHttpClient时通过cache()指定一个目录和大小上限。以下 Android 示例设置 50 MiB(文档原注释:2020 年值约 5 美分的手机存储空间):
val client = OkHttpClient.Builder() .cache(Cache( directory = File(application.cacheDir, "http_cache"), maxSize = 50L * 1024L * 1024L // 50 MiB )) .build()两个要点:
- 缓存目录必须被单个 Cache 实例独占,但同一个 Cache 可被多个
OkHttpClient共享; - 缓存设计用于跨应用重启持久化,无需每次启动都清空。
相关源码:Cache.kt,官方缓存文档见 docs/features/caching.md。
三种命中场景:Hit、Miss 与 Conditional Hit
OkHttp 通过EventListener暴露缓存事件,三种典型场景如下(完整事件流见 docs/features/events.md):
| 场景 | 事件序列 | 是否走网络 | 说明 |
|---|---|---|---|
| Cache Hit | CallStart →CacheHit→ CallEnd | 否 | 理想情况,直接跳过 DNS、建连、下载 |
| Cache Miss | CallStart →CacheMiss→ 常规网络事件 | 是 | 首次请求、不可缓存或已过期 |
| Conditional Hit | CallStart →CacheConditionalHit→ 网络事件 → 响应体 0 字节 →CacheHit | 是(极小) | 服务器确认未修改,返回 304 |
上图展示了正常一次调用的事件顺序;当缓存命中时,绿色"建连"区域和紫色"响应体下载"部分都会被大幅跳过,这正是省时间的来源。
新鲜度怎么算?(RFC 9111 的新鲜性规则)
判断"还能不能直接用缓存"的逻辑在 CacheStrategy.kt 中,核心规则:
- 响应带
Cache-Control: max-age→ 按该时长计算新鲜期; - 否则带
Expires头 → 按到期时间计算; - 否则带
Last-Modified头 → 采用 RFC 建议的启发式新鲜期:文档年龄(响应时间减去最后修改时间)的10%; - 带 query 参数的 URL 不使用默认过期时间(这类 URL 通常指向动态内容)。
条件缓存命中:ETag 如何帮你省流量
这是 OkHttp 响应缓存最精髓的部分。当缓存副本过期(stale)但携带ETag或Last-Modified时,OkHttp 不会傻乎乎地全量重新下载,而是发起条件请求:
- 有
ETag→ 请求头加上If-None-Match: <etag>(优先); - 否则有
Last-Modified→ 加上If-Modified-Since: <date>。
服务器对比后发现资源没变,就返回304 Not Modified,不携带响应体——OkHttp 此时把本地缓存副本作为最终响应返回。一次几 MB 的文件更新检查,流量可能只有几十字节的请求头。响应中cacheResponse与networkResponse均非空,且仅当状态码为 304 时缓存副本才作为顶层响应。
源码实现见 CacheStrategy.kt 中"选择校验条件"的分支逻辑。
💡 想让缓存效率最大化?在服务器端给静态资源返回
ETag和合理的max-age是最有效的手段。
三种常用 CacheControl 指令
CacheControl.kt 提供了三类常用指令,配合刷新/离线功能非常好用:
| 指令 | 用途 |
|---|---|
noCache | 强制走网络全量刷新(用户点击"刷新"按钮时使用) |
maxAge(0) | 只要求服务器校验缓存,比 noCache 更省流量 |
onlyIfCached | 只读本地缓存,无缓存则直接返回 504,适合离线模式 |
缓存管理与命中率统计
Cache实例跟踪三个关键指标,帮你量化缓存效果:
- requestCount:累计请求数;
- networkCount:走了网络的请求数;
- hitCount:由缓存满足的请求数。
注意:条件命中会同时计入 networkCount 和 hitCount——因为既有网络交互也有缓存命中。日常运维操作:
cache.evictAll():清空全部缓存释放空间;- 遍历
cache.urls()可按 URL 前缀精确删除(如下拉刷新后只清某个域名); cache.delete():彻底删除目录,一般只在卸载时调用。
常见坑:为什么缓存没生效?
最常见的原因:响应体没有读完。只有被完整读取、取消或停滞的响应才会写入缓存,请确保try-with-resources关闭了 Response。其他注意点:
- 请求或响应任一端的
no-store指令都会禁止缓存; - 3xx 重定向类响应需要
Expires、max-age或public/private头才允许缓存; - 如果请求自己带了
If-None-Match等条件头,内置缓存将不参与,由你的代码自行处理。
更多细节见官方缓存文档 docs/features/caching.md 的 Troubleshooting 章节,规范遵循情况见 README.md 中对 RFC 9110 / 9111 / 9112 的说明。
小结
- OkHttp 响应缓存遵循RFC 9111,默认关闭,
cache(Cache(目录, 大小))一行开启; - 新鲜期内直接命中省掉全部网络;过期后条件命中(ETag + 304)让响应体归零;
- 善用
maxAge(0)/onlyIfCached平衡刷新与离线体验; - 记住"响应要读完才入库",并用 hitCount 验证缓存收益。
掌握这套机制后,你的应用可以在不牺牲数据实时性的前提下,显著降低流量消耗与首屏延迟。
【免费下载链接】okhttpA meticulous HTTP client for the JVM, Android, and GraalVM.项目地址: https://gitcode.com/gh_mirrors/okh/okhttp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考