1. HTTP 缓存头基础:Cache-Control、Expires、ETag、Last-Modified、Pragma
HTTP 缓存头是浏览器与服务器之间控制资源缓存行为的关键协议。正确配置这些头部能显著减少重复请求、降低延迟、节省带宽,是移动端网络优化的第一道防线。在 Retrofit + OkHttp 的缓存架构中,这些头部决定了缓存是否命中、是否重新验证以及缓存有效期。本章将从原理、语法、优先级、实现配置、调试方法、常见错误和性能优化七个维度逐一拆解五个核心缓存头。
1.1 缓存头核心原理与对比
五个头部按作用分为两类:强缓存(无需请求服务器)和协商缓存(需与服务器验证)。下表列出每个头部的机制、优先级和典型值。
| 头部名称 | 分类 | 作用机制 | 典型值示例 | 优先级 |
|---|---|---|---|---|
Cache-Control | 强缓存 | 指令集,控制缓存行为(max-age、no-cache、no-store 等) | max-age=3600, public | 最高(覆盖 Expires、Pragma) |
Expires | 强缓存 | 过期时间(HTTP/1.0 遗产) | Wed, 21 Oct 2025 07:28:00 GMT | 低于 Cache-Control max-age |
Pragma | 强缓存 | 仅 no-cache 指令(HTTP/1.0 遗产) | no-cache | 最低(已废弃) |
ETag | 协商缓存 | 资源唯一标识(哈希或版本号),用于验证资源是否变化 | "686897696a7c876b7e" | 与 Last-Modified 并存时优先 |
Last-Modified | 协商缓存 | 资源最后修改时间(秒级精度) | Tue, 15 Oct 2024 10:30:00 GMT | 低于 ETag |
优先级原则:Cache-Control(尤其是
max-age)优先级最高;若同时存在 Expires 和 max-age,则 max-age 生效。Pragma 仅在 HTTP/1.0 客户端中向后兼容。
1.2 缓存验证流程(内联 SVG)
下图展示了客户端发起请求时,强缓存与协商缓存的完整判断流程。其中 ETag 和 Last-Modified 在缓存过期后用于向服务器发起条件请求。
1.3 配置与代码实现(Retrofit + OkHttp)
在 Android 项目中,通过 OkHttp 的 Cache 对象和拦截器可以自动处理缓存头。下面演示如何配置客户端以尊重服务器的缓存指令。
// OkHttp 缓存配置
val cacheSize = 10 * 1024 * 1024L // 10 MB
val cache = Cache(cacheDir, cacheSize)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.addInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
// 强制缓存 60 秒(仅示例,实际应遵循服务器 Cache-Control)
originalResponse.newBuilder()
.header("Cache-Control", "max-age=60")
.removeHeader("Pragma")
.build()
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.build()
注意:生产环境不要强制覆盖服务器头部,上述拦截器只作演示。通常应保留服务器原始
Cache-Control,或根据业务需求使用 @Headers 注解添加。
1.4 调试方法
使用 curl 或浏览器开发者工具验证缓存头是否正确返回。
# 查看响应头中的缓存信息
curl -I https://api.example.com/users
# 输出示例
HTTP/2 200
cache-control: max-age=3600, public
etag: "v2-abc123"
last-modified: Mon, 10 Mar 2025 12:00:00 GMT
expires: Mon, 10 Mar 2025 13:00:00 GMT
在 Android 中,可通过 OkHttp 的日志拦截器打印请求/响应头:
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS
}
OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
1.5 常见错误与避坑
- Cache-Control 与 Expires 同时存在导致混乱: 确保
max-age与Expires时间一致,或完全依赖Cache-Control。 - Pragma: no-cache 被误用: 仅用于向下兼容 HTTP/1.0,现代浏览器忽略它。应使用
Cache-Control: no-cache。 - ETag 弱校验 vs 强校验: 弱 ETag 以
W/开头(如W/"v2"),允许语义等价但字节不同的内容。强 ETag 要求字节级相同。 - Last-Modified 精度不足: 秒级精度可能无法检测到秒内的多次修改,推荐同时使用 ETag。
- 移动端网络不稳定导致缓存失效: OkHttp 默认不缓存带
no-store的响应,请确认服务器返回正确指令。
1.6 性能与稳定性优化建议
- 合理设置 max-age: 静态资源(图片、CSS、JS)可设置较长过期时间(如 7 天),动态 API 建议 60-300 秒或使用
no-cache并依赖 ETag。 - 使用 ETag 替代 Last-Modified: ETag 更精准,避免秒级粒度过粗带来的无效验证。
- 在 OkHttp 中启用缓存并设置合理大小: 根据应用类型调整(图片类 50MB,数据类 10-20MB)。
- 结合离线策略: 使用
CacheStrategy或自定义拦截器实现“先缓存后网络”模式,提升弱网体验。 - 监控缓存命中率: 通过 OkHttp 的
Cache.getHitCount()和Cache.getMissCount()分析效率,调整缓存策略。
核心结论: Cache-Control 是现代缓存控制的核心,ETag 是最佳协商验证方案,Expires 和 Pragma 仅作向后兼容。合理组合这些头部能最大程度减少网络开销。