先说业务使用上的结论
- Okhttp 只允许GET请求使用缓存
- okhttp 在使用缓存的时候,url 不能携带可变参数,否则无法命中缓存,如每次发起请求的时候,url内会包含一个时间戳,那么当前请求,无法命中缓存
OkHttp 缓存机制深度解析与 SOP 操作指南
本文基于 OkHttp 4.9.3 源码,深度解析其缓存设计逻辑,并提供实践指南。
一、缓存机制概述
1. 核心原则
- 仅缓存 GET 请求:符合 HTTP 语义安全要求
- 双重校验机制:存储 (Write) 与读取 (Read) 阶段双重过滤
- 自动失效处理:对写操作(如 POST)自动清理相关缓存
2. 关键类说明
类名 | 作用 |
---|---|
CacheInterceptor |
拦截请求,协调缓存与网络决策 |
Cache |
管理磁盘缓存存储 |
CacheStrategy |
根据请求/响应头计算缓存策略 |
二、缓存工作流程图解
A[发起请求] --> B[CacheInterceptor]
B --> C{是否存在缓存?}
C -- 有 --> D[检查缓存有效性]
C -- 无 --> E[发起网络请求]
D -- 有效 --> F[返回缓存]
D -- 无效 --> E
E --> G{是否为GET请求?}
G -- 是 --> H[存储响应到缓存]
G -- 否 --> I[跳过存储]
三、源码深度解析
- 缓存拦截入口:CacheInterceptor源码位置:okhttp3/internal/cache/CacheInterceptor.kt
override fun intercept(chain: Interceptor.Chain): Response {
// 1. 尝试读取缓存
val cacheCandidate = cache?.get(chain.request())
// 2. 计算缓存策略
val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
// 3. 决策分支
when {
strategy.networkRequest == null && strategy.cacheResponse == null -> {
// 强制返回 504 无网络且无缓存
}
strategy.networkRequest == null -> {
// 返回缓存(不发起网络请求)
}
else -> {
// 发起网络请求并可能更新缓存
}
}
}
- 缓存存储逻辑 (Cache.kt)
// 路径:okhttp3/internal/cache/Cache.kt
internal fun put(response: Response): CacheRequest? {
// 规则 1:仅处理 GET 请求
if (response.request.method != "GET") return null
// 规则 2:处理写操作缓存失效(POST/PUT/DELETE 等)
if (HttpMethod.invalidatesCache(response.request.method)) {
remove(response.request) // 删除关联缓存
return null
}
// 规则 3:生成唯一缓存 Key
val key = key(response.request.url) // 即 url.toString()
// 磁盘写入操作(使用 DiskLruCache)
val entry = Entry(response)
return try {
val editor = cache.edit(key) ?: return null
entry.writeTo(editor)
RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
null
}
}
- 缓存策略决策 (CacheStrategy.kt)
// 路径:okhttp3/internal/cache/CacheStrategy.kt
internal class Factory(
private val request: Request,
private val cacheResponse: Response?
) {
fun compute(): CacheStrategy {
// 强制不缓存场景(如 no-store)
if (request.cacheControl.noStore || cacheResponse?.cacheControl?.noStore == true) {
return CacheStrategy(request, null)
}
// 缓存有效性验证
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
// 计算缓存新鲜度
val ageMillis = cacheAgeMillis()
val freshMillis = cacheResponseMaxAgeMillis()
val maxStaleMillis = if (request.cacheControl.maxStale != -1) {
request.cacheControl.maxStale * 1000L
} else Long.MAX_VALUE
return when {
ageMillis + minFreshMillis < freshMillis + maxStaleMillis ->
CacheStrategy(null, cacheResponse)
else ->
CacheStrategy(request.newBuilder().build(), cacheResponse)
}
}
}
四、缓存配置 SOP
步骤 1:初始化缓存
// 创建 10MB 缓存实例
val cacheSize = 10 * 1024 * 1024L // 10MB
val cache = Cache(
directory = File(context.cacheDir, "okhttp_cache"),
maxSize = cacheSize
)
// 注入 OkHttpClient
val client = OkHttpClient.Builder()
.cache(cache)
.build()
步骤 2:服务端缓存控制
# 响应头示例(缓存 1 小时)
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "xyz123"
Last-Modified: Wed, 21 Oct 2022 07:28:00 GMT
步骤 3:客户端强制刷新
val request = Request.Builder()
.url(url)
.header("Cache-Control", "no-cache") // 强制网络验证
.build()
五、常见问题排查表
现象 | 可能原因 | 解决方案 |
---|---|---|
缓存完全不生效 | 1. 未配置 Cache 实例2. 服务端未返回有效的缓存头(如 Cache-Control )3. 缓存目录权限不足 |
1. 检查 OkHttpClient 是否注入 Cache 对象2. 确保响应头包含 Cache-Control: max-age=xxx 3. 检查磁盘读写权限 |
部分请求无法缓存 | 1. 请求方法非 GET 2. URL 含随机参数(如 ?_t=timestamp )3. 响应状态码非法(如非200) |
1. 仅对 GET 请求使用缓存2. 自定义缓存 Key 过滤随机参数 3. 确保服务端返回 200/301 等可缓存状态码 |
磁盘缓存异常增长 | 1. 未清理过期缓存 2. 相同资源重复缓存(URL 参数不同) 3. 缓存最大尺寸设置过大 |
1. 调用 cache.evictAll() 清理2. 统一 URL 参数规范 3. 调整 maxSize 为合理值(如10MB) |
缓存数据过期但未更新 | 1. 缓存时间设置过长 2. 未使用 ETag /Last-Modified 验证机制3. 客户端强制缓存过期数据 |
1. 缩短 max-age 时间2. 添加条件请求头验证 3. 调用 request.cacheControl(CacheControl.FORCE_NETWORK)
|
POST请求后GET缓存未失效 | 1. 未触发 HttpMethod.invalidatesCache 机制2. 服务端未更新资源版本 |
1. 确认 POST 等写操作后 OkHttp 自动调用 remove() 2. 服务端更新 ETag 或 Last-Modified
|
六、源码设计精髓总结
双检锁机制
在 CacheInterceptor 和 CacheStrategy 中多次验证缓存有效性,确保逻辑严谨性。
磁盘缓存优化
采用 DiskLruCache 实现:
使用 LRU 淘汰算法
通过 journal 文件保证原子性
写入采用临时文件+原子提交
HTTP 协议完备性
完整支持:
条件请求 (If-Modified-Since/ETag)
Vary 头处理
分块传输编码缓存