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 在缓存过期后用于向服务器发起条件请求。

HTTP 缓存验证流程 客户端发起请求 检查强缓存(Cache-Control/Expires) 命中 未命中 使用本地缓存 发起请求(含 If-None-Match / If-Modified-Since) 服务器:比较 ETag / Last-Modified 304 Not Modified 200 OK(新资源) 仍使用本地缓存 + 更新过期时间 更新缓存并返回新资源 说明:当强缓存命中时直接使用本地缓存,无需请求服务器。协商缓存通过 ETag / Last-Modified 向服务器验证, 返回 304 则继续使用缓存,返回 200 则更新缓存。

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 仅作向后兼容。合理组合这些头部能最大程度减少网络开销。