CacheStrategy#computeCandidate()
computeCandidate()主要用于计算如下三种情况
// A
return CacheStrategy(request, null)
// B
return CacheStrategy(null, builder.build())
// C
return CacheStrategy(conditionalRequest, cacheResponse)
如果收到的request中携带来only-if-cached,那么在CacheStrategy#compute()中,A/C会变成D
// D
return CacheStrategy(null, null)
computeCandidate()内部执行流程
- 判定A(筛选只能发起网络请求的情况)
- 判定B(筛选可以复用的情况)
- 判定C(剩下的就帮它发起网络请求,除非没法生成validator)
判定A
// 1
// No cached response.
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// 2
// Drop the cached response if it's missing a required handshake.
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
// 3
// If this response shouldn't have been stored, it should never be used as a response source.
// This check should be redundant as long as the persistence store is well-behaved and the
// rules are constant.
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
val requestCaching = request.cacheControl
if (
// 4
requestCaching.noCache
// 5
|| hasConditions(request)
) {
return CacheStrategy(request, null)
}
A-1
cacheReponse不为空时满足:
- URI match
- method match
- varys match
这里显然就是在检查是否有满足reuse的基本条件(rfc 7234 4. Constructing Responses from Caches )
A-2
OKHTTP_TODO
A-3
fun isCacheable(response: Response, request: Request): Boolean {
//...其他代码
return !response.cacheControl.noStore && !request.cacheControl.noStore
}
因为在执行store操作的时候也会调用isCacheable(),所以当执行到A-4的时候isCacheable()方法等同于:
fun isCacheable(response: Response, request: Request): Boolean {
return !request.cacheControl.noStore
}
这表示当OkHttp收到request(no-store)时会直接发起网络请求而不会继续判定是否能reuse。这样实现并 没有违反rfc 7234的定义 ;因为其中并没有对“当cache 收到了request(no-store)时应该怎么做”进行定义。
gemini说OkHttp这么实现是因为当收OKHttp到request(no-store)时认为(OKHTTP_TODO):
- 用户不想缓存对应的response
- 用户不想结果被缓存干扰
A-4
5.2.1.4. no-cache
The "no-cache" request directive indicates that a cache MUST NOT use
a stored response to satisfy the request without successful
validation on the origin server.
rfc 72345.2.1.4. no-cache 中定义,当cache收到request(no-cache) 时不能进行resuse流程,除非先对对应stored response进行validation。我目前将“validation”理解为 “为这个stored response构造一个conditional request,发往服务器进行验证”。
但是OkHttp遇到这种情况却选择跳过接下来的reuse判断,然后直接将收到的request发往下一跳;
// rfc定义
reuse -> stored response
validation -> 1. 304 -> 更新header -> 返回stored response
2.完整response -> 删除缓存/保存最新的 -> 返回stored response
// okhttp中实现
直接转发request到下一跳 -> store流程 -> 返回收到的response
OkHttp实现no-store request指令的原因猜测:
- 与rfc定义相比,OkHttp的实现需要处理的分支变少了,并且可能会消耗更多的流量。但是这种实现增加了no-store request指令在OkHttp中可预测性增加; 同时简化了实现。
- gemini说开发中在request使用no-store指令的时候,期望cache不要让复用逻辑来干扰对no-store指令结果的预测
A-5
OKHttp对于conditional requst的处理如下:
- OkHttp发现有If-Modified-Since/If-None-Match时就会明确的将request转发到下一跳
- 其他conditional headers一律当不存在;
- OkHttp不会缓存response(206)
显然第二条会导致client收到的结果和预期不符并且出现一些问题。
private fun hasConditions(request: Request): Boolean =
request.header("If-Modified-Since") != null || request.header("If-None-Match") != null
“If-Modified-Since/If-None-Match”:
虽然 rfc 7234 4.3.2. Handling a Received Validation Request 中允许cache可以处理conditional header,但是当OkHttp遇到这2个conditional header时,选择将request直接转发。
gemini说此时OkHttp认为:
- 虽然rfc定义了如何处理但是这增加了实现的复杂度
- 同时实现会降低结果的准确性
If-Match/If-Ummodified-Since:
OkHttp没有对这2个conditional header进行判断;当cache收到request(If-Match)这导致了一些有趣的情况:
- cache中已经有request-response
- cache收到request(If-Match)
- computeCandidate()中reuse逻辑判断通过,cache选择将stored response返回给client;或者
- reuse()逻辑未通过,OkHttp根据stored response的validator尝试手动构建条件请求;或者
- OkHttp构建条件请求失败,最终选择直接将request转发到下一跳。
当client发起request(If-Match)时,期望去orignal server验证。但是在OkHttp当实现中,只有5.才能实现client的期望。
Range:
OkHttp默认不缓存response(206)。
当本地有request-response时:
- cache收到request(Range)
- computeCandidate()中reuse逻辑判断通过,cache选择将完整的stored response(code)返回给client;或者
- reuse()逻辑未通过,OkHttp根据stored response的validator尝试手动构建条件请求;或者
- OkHttp构建条件请求失败,最终选择直接将request转发到下一跳。
client期望:
- 收到206/416/其他 response code(RFC_GUESS)
- 要求的范围
当reuse逻辑通过时:
- 返回完整的store response(cacheable code)
If-Range/Range: 当本地有request-response(v1)时:
- cache收到request(Range,If-Range(v2))
- computeCandidate()中reuse逻辑判断通过,cache选择将完整的stored response(v1)返回给client;或者
- reuse()逻辑未通过,OkHttp根据stored response的validator(v2)尝试手动构建条件请求;或者
- OkHttp构建条件请求失败,最终选择直接将request转发到下一跳。
当resuse()逻辑通过时,If-Range(v2) 期望V2版本,但是OkHttp却返回了v1版本给client。这是个明显的错误。不知道为什么OkHttp要这么实现(OKHTTP_TODO)
判定B
val responseCaching = cacheResponse.cacheControl
// 1
val ageMillis = cacheResponseAge()
//2
var freshMillis = computeFreshnessLifetime()
if (requestCaching.maxAgeSeconds != -1) {
freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
}
var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}
var maxStaleMillis: Long = 0
if (
// 3
!responseCaching.mustRevalidate
&& requestCaching.maxStaleSeconds != -1
) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}
if (
// 4
!responseCaching.noCache
&&
// 5
ageMillis + minFreshMillis < freshMillis + maxStaleMillis
) {
val builder = cacheResponse.newBuilder()
// 6
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection "Response is stale"")
}
// 7
val oneDayMillis = 24 * 60 * 60 * 1000L
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection "Heuristic expiration"")
}
return CacheStrategy(null, builder.build())
}
B-1
参考CacheStrategy#cacheResponseAge()
B-2
参考CacheStrategy#computeFreshnessLifetime()
B-3
must-revalidate表示如果不能给client提供stale的副本,除非对其进行了验证(rfc 7234 5.2.2.1. must-revalidate)。 max-stale明显违反must-revalidate的语义,所以OkHttp在遇到must-revalidate时选择不计算max-stale
B-4
rfc 7234 5.2.2.2. no-cache 中response(no-cache)处理分为带值和不带 值的情况,OkHttp则统统按照不带值进行处理:如果response中有no-cache指令,则直接进入“通过validator构造条件请求”逻辑。
B-5
B除代码用于判断是否可以reuse cacheResponse;
ageMillis + minFreshMillis < freshMillis + maxStaleMillis公式的推导:
思路是依次使用Cache-Control对lifetime进行约束计算出新的adjustedLifetime。当相关约束都进行了计算以后,将最终的adjustedLifetime带入 rfc 7234 4.2. Freshness 中定义的新鲜度判定公式中
response_is_fresh = (freshness_lifetime > current_age)
可以得出OkHttp代码中使用的判断公式
age/freshness lifetime
// rfc中定义的新鲜度判定公式
current_age < lifetime
// 满足max-age
current_age <= max-age
adjustedLifetime = min(lifetime,max-age)
// 满足max-stale
current_age <= lifetime + max-stale
adjustedLifetime = min(lifetime,max-age) + max-stale
// 满足 min-fresh
current_age <= lifetime - min-fresh
adjustedLifetime = min(lifetime,max-age) + max-stale - min-fresh
// 变形
ageMillis + min-fresh < min(lifetime,maxage) + max-stale
// 最终得出代码中相同判断
ageMillis + minFreshMillis < freshMillis + maxStaleMillis
目前的理解:
- 上述推导是我猜的
- ageMillis + minFreshMillis < freshMillis + maxStaleMillis
B-6/B-7
这两块代码就有缘在来搞懂吧。。
C
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
val conditionName: String
val conditionValue: String?
when {
etag != null -> {
conditionName = "If-None-Match"
conditionValue = etag
}
lastModified != null -> {
conditionName = "If-Modified-Since"
conditionValue = lastModifiedString
}
servedDate != null -> {
conditionName = "If-Modified-Since"
conditionValue = servedDateString
}
else -> return CacheStrategy(request, null) // No condition! Make a regular request.
}
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
return CacheStrategy(conditionalRequest, cacheResponse)
这部分代码用于根据当A/B筛选都没通过时,根据cacheResponse的validator帮它手动构建一个conditional request。