CacheControl 与缓存指令:max‑age、no‑cache、no‑store、only‑if‑cached
1. 概念与原理
HTTP Cache-Control 头部是客户端与服务器协商缓存行为的核心机制。OkHttp 通过 CacheControl 类封装这些指令,开发者可以精确控制请求应如何使用缓存,以及响应应如何被缓存。指令分为两大类:请求指令(客户端发往服务器,影响中间缓存和源站的处理方式)与 响应指令(服务器返回给客户端,限定本地缓存策略)。本章重点介绍四个最常用的指令:max‑age、no‑cache、no‑store、only‑if‑cached。
核心认知:
no-cache 名字易误导——它并不禁止缓存,而是强制每次使用缓存前都向服务器验证资源是否新鲜。真正禁止缓存的是 no-store。
| 指令 | 类型 | 含义 | 适用方向 |
|---|---|---|---|
max-age=s | 请求/响应 | 指定缓存的最大有效秒数 | 请求:告知服务器愿意接受多久内未过期的缓存 响应:告知缓存端该资源在 s 秒内无需验证 |
no-cache | 请求/响应 | 强制每次使用缓存前向服务器重新验证 | 请求:要求服务器返回完整响应,不得使用缓存 响应:缓存可以存储,但每次使用必须验证 |
no-store | 请求/响应 | 禁止任何形式的缓存存储(内存/磁盘) | 请求/响应:缓存必须忽略该资源 |
only-if-cached | 请求 | 只从缓存返回,如果缓存未命中则返回 504 Gateway Timeout | 请求:适用于纯离线模式,不发起网络请求 |
2. 流程图:指令如何影响缓存决策
下图展示请求携带不同 Cache-Control 指令时,OkHttp 缓存拦截器的核心判断分支。注意 only-if-cached 会跳过网络请求,no-cache 会导致即使有缓存也要重新验证。
3. OkHttp 中的 CacheControl API
OkHttp 提供 CacheControl.Builder 来构建指令,通过 Request.Builder.cacheControl() 或 Response.cacheControl() 应用。以下示例展示常用指令组合:
// 构建请求:max-age=60,且只接受缓存(only-if-cached)
val cacheControl = CacheControl.Builder()
.maxAge(60, TimeUnit.SECONDS)
.onlyIfCached()
.build()
val request = Request.Builder()
.url("https://api.example.com/data")
.cacheControl(cacheControl)
.build()
// 响应端检查指令
val response = client.newCall(request).execute()
val responseCC = response.cacheControl()
println("max-age = ${responseCC.maxAgeSeconds}") // 或 -1 表示未设置
println("no-cache = ${responseCC.noCache}") // true/false
println("no-store = ${responseCC.noStore}") // true/false
println("only-if-cached = ${responseCC.onlyIfCached}") // 仅在请求有效
注意:
only-if-cached 只能作为请求指令使用,如果服务器或中间缓存未命中,OkHttp 将返回 504 状态码且不发起网络请求。此指令非常适合离线模式或飞行模式下的降级策略。
4. 实现流程:在缓存拦截器中注入自定义 CacheControl
通过自定义 Interceptor,可以在运行时动态添加或修改 CacheControl 指令。典型场景:根据网络状态(WiFi/移动数据/离线)切换缓存策略。
- 创建拦截器,实现
Interceptor接口。 - 在
intercept()方法中获取原始Request。 - 构造新的
CacheControl,合并业务逻辑(例如:当前为弱网络时增加 max‑age,检测到离线强制 only‑if‑cached)。 - 使用
request.newBuilder().cacheControl(newCC).build()替换请求。 - 调用
chain.proceed(newRequest)获取响应。 - 可选:根据响应状态或业务需求修改响应端缓存指令(例如替换
Cache-Control响应头)。
class DynamicCacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// 假设当前网络为离线状态
val isOffline = true
val newCacheControl = if (isOffline) {
CacheControl.Builder()
.onlyIfCached()
.maxAge(Int.MAX_VALUE, TimeUnit.SECONDS) // 允许使用任何过期的缓存
.build()
} else {
CacheControl.Builder()
.maxAge(30, TimeUnit.SECONDS)
.build()
}
val modifiedRequest = originalRequest.newBuilder()
.cacheControl(newCacheControl)
.build()
val response = chain.proceed(modifiedRequest)
// 若想在响应端强制 no-cache,可修改响应头(需使用 Response.Builder)
return response
}
}
5. 调试与验证
- 日志拦截器:使用
HttpLoggingInterceptor输出完整的请求/响应头,查看Cache-Control是否按预期发送。 - OkHttp 事件监听器:通过
EventListener的cacheHit()/cacheMiss()事件判断缓存决策是否正确。 - 抓包工具:使用 Charles 或 Wireshark 抓取 HTTP 报文,验证
Cache-Control指令的头部值。
// 日志拦截器示例
val logger = HttpLoggingInterceptor { message -> Log.d("OKHTTP", message) }
logger.setLevel(HttpLoggingInterceptor.Level.HEADERS)
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.cache(Cache(File(cacheDir, "http_cache"), 5L * 1024 * 1024))
.addInterceptor(DynamicCacheInterceptor())
.build()
6. 常见错误与陷阱
| 错误 | 后果 | 正确做法 |
|---|---|---|
将 no-cache 当作“禁用缓存” | 缓存仍然存储了资源,但被频繁验证 | 需禁用缓存时使用 no-store |
仅设置了响应端 max-age,但请求端未配合 | 客户端可能因其他原因忽略响应指令(如 private 等) | 明确在响应头中提供完整的 Cache-Control |
only-if-cached 未与缓存过期时间联合使用 | 旧数据可能被无限期返回 | 配合 max-stale 或 max-age 控制陈旧容忍范围 |
混淆 max-age 与 Expires | 优先级错误(max-age 高于 Expires) | 统一使用 Cache-Control,废弃 Expires |
7. 性能与稳定性优化
- 合理设定 max‑age:对于不常变动的静态资源(图标、CSS),设置较长值(如 86400 秒)可大幅减少带宽消耗与延迟。
- 离线降级:监听网络变化,在断网时启用
only-if-cached+max-stale(Int.MAX_VALUE),让应用仍然可用。 - 动态切换策略:根据用户行为(如滑动列表)区分即时数据与背景数据;即时数据使用
no-cache,背景数据使用max-age。 - 避免过度使用 no-store:每次请求都走网络会增加服务器压力和网络延迟,仅在敏感信息(如、支付表单)中使用。
实践建议: 在 OkHttp 的缓存拦截器中,将 CacheControl 指令与
NetworkInfo、用户偏好、资源类型解耦,通过策略对象(Strategy Pattern)灵活构建不同的指令组合,便于测试和维护。