本文中源码基于OkHttp 3.6.0
- 《OkHttp Request 请求执行流程》
- 《OkHttp - Interceptors(一)》
- 《OkHttp - Interceptors(二)》
- 《OkHttp - Interceptors(三)》
- 《OkHttp - Interceptors(四)》
本文主要分析 OkHttp 中的 CacheInterceptor 对缓存的处理。
在实际的网络请求过程中,一份响应数据在一定时间内可能并不会发生修改,如果每次响应都传输同一份数据就会造成冗余的数据传输,浪费服务器的带宽,同时也增加了服务器的性能压力。
那么为了解决这些问题,HTTP 提供了缓存这一机制,在得到原始的响应数据后,本地或者缓存服务器保留原始响应数据的一个副本,如果后续再发起相同的请求,则将响应的副本直接返回给请求方,从而提高响应速度、并减少服务器的压力。
引入缓存后,就需要处理缓存是否命中、缓存是否新鲜等情况,我们可以从下图看看缓存的处理流程。
缓存处理流程
为了让客户端能够判断缓存是否过期,Http 中定义了多个 Header 值来描述缓存的过期时间、以及用来进行服务器缓存再验证。
缓存过期时间:
- Cache-Control: max-age,描述响应缓存能够存活的最大时间,response 从生成到不再新鲜的时间;
- Expires,描述缓存过期的绝对时间,如果系统时间超过该时间则表示缓存不再新鲜。
Expires 是 HTTP/1.0+ 定义的 Header,Cache-Control 是 HTTP/1.1 中定义的 Header,它们的本质是一样的,都是用于描述缓存的过期时间,它们最大的区别是 Cache-Control 描述相对时间,Expires 描述绝对时间。
从之前的流程图中知道,在缓存模块匹配到 Request 的缓存后需要判断缓存是否过期,HTTP 中使用缓存的存活时间和新鲜时间来进行判断:
- 存活时间:表示服务器发布原始响应后经过的总时间;
- 新鲜时间:缓存在过期之前能过存活的时间。
如果存活时间小于新鲜时间,则表示缓存未过期,反之表示缓存过期,需要进行验证。
条件验证:
- If-Modified-Since: <Date>,服务器在执行请求时通常会在 Response 中包含一个 Last-Modified 首部表示文档的修改时间,在 If-Modified-Since 后跟上这个时间就能让服务器进行验证文档的有效性了。
- If-None-Match: <Tag>,有时通过时间进行验证是不够的,Response 中通常会使用 ETag 对实体进行标记,在条件验证时,服务器通过判断标签是否发生变化来确定文档的有效期。
在执行条件验证的时候,在 Request 的 Header 中加上验证条件,服务器在收到请求后,会判断条件是否满足,如果在指定条件下,服务器上的文旦内容发生了改变,服务器将执行一个原始的请求并返回最新的文档数据;如果文档未发生改变,则服务器会返回一个 304 Not Modified 报文,并返回一个新的过期时间用于更新缓存。
- CacheInterceptor
下面我们看看 OkHttp 中 CacheInterceptor 对缓存的处理,先上源码。
public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}
return response;
}
初看这段代码真的很让人崩溃,特别是那个 CacheStrategy
,实在让人摸不着头脑,完全看不出它有任何策略模式的影子。
我们暂时先猜测构建 CacheStrategy 的目的只是为了修改 Request 和 Response 中的属性。
后面的条件判断逻辑也很模糊不清,各种判空作为逻辑条件,如果不去看 CacheStrategy 的源码,不知道缓存的处理流程的话,基本搞不懂这些判断是什么意思。。。
吐槽完了还是得继续,那么我们结合前面分析的缓存处理过程,重新来梳理一遍这段代码。
首先是匹配缓存,这一步很简单。
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
这里判断用户是否设置了 Cache,如果存在 Cache,则从 Cache 中匹配当前请求的缓存。
用户可以通过 OkHttpClient 中设置 Cache,其中需要制定缓存的存放路径和缓存的最大容量。
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(Environment.getDownloadCacheDirectory(), 1024 * 1024)).build();
按道理讲,接下来应该判断 是否命中缓存,从而决定是直接请求网络数据还是判断缓存是否过期,但 CacheInterceptor 中并没有这么做,而是构建了一个 CacheStrategy,实际上它是将缓存的合法性、缓存是否过期等判断全部放到 CacheStrategy 的构建过程中来做了。
public Factory(long nowMillis, Request request, Response cacheResponse) {
// 当前发起请求的时间
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
// 获取cacheResponse中用于缓存信息的Header和属性,这些值主要是用于计算缓存的存活时间和新鲜时间
if (cacheResponse != null) {
// 获取缓存请求发起的时间
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
// 获取缓存响应接收的时间
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
// 服务器响应缓存请求的时间
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
// 缓存的过期时间
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
// 缓存文档上次发生修改的时间
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
// 缓存文档的实体标记
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
// 缓存在网络中间节点的存活时间
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
这里获取了缓存的一些基本信息,用于后面计算缓存的存活时间和缓存的新鲜时间,用于判断缓存是否过期。
下面根据条件构造 CacheStrategy。
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
private CacheStrategy getCandidate() {
// 如果没有命中缓存,直接使用网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 丢弃缓存,如果缓存缺失三次握手的话(至于什么时候会出现这种情况,并没有深究)
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 这里判断Response的Code和header中是否禁用缓存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
// noCache 并非不让缓存的意思,它表示请求强制要求执行条件验证;如果请求的Header中包含了”If-Modified-Since”
// 或“If-None-Match”,同样表示强制条件验证
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 计算缓存的存活时间
long ageMillis = cacheResponseAge();
// 计算缓存的新鲜时间
long freshMillis = computeFreshnessLifetime();
// 如果请求中设置了缓存的最大有效期
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
// 缓存未过有效期,直接使用缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// 缓存已过有效期,这里构造一个条件验证的 Request
String conditionName;
String conditionValue;
// 使用 ETag 实体验证
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
// 使用有效期验证
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
// 缓存中不包含用于条件验证的 Header,直接使用网络请求
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
上面就是构造 CacheStrategy 的地方了,可以看到在构造 CacheStrategy 的时候,一共有4种情况,这4种不同的构造方法分别对应了 CacheInterceptor 中对缓存的4种不同处理策略。
-
new CacheStrategy(null, null)
:请求中强制使用缓存,但缓存并不存在; -
new CacheStrategy(request, null)
:缓存不存在或不可用,使用网络请求; -
new CacheStrategy(null, response)
:缓存还未过期,直接使用缓存; -
new CacheStrategy(request, response)
:缓存过期,需要进行条件验证。
现在再回过头去看 CacheInterceptor 的 intercept 中的条件判断逻辑,应该就清楚多了。
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
当 Request 中强制要求使用缓存,但缓存并不存在时,构造一个 504 错误。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
缓存任然未过期,不需要使用网络请求,直接返回缓存。
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
当程序运行到这里时,有两种可能:一是直接发起网络请求,获取原始数据(networkRequest != null && cacheResponse == null)
;二是需要进行条件验证(networkRequest != null && cacheResponse != null)
。
如果是第二种情况,则判断验证是否成功。
if (cacheResponse != null) {
// 如果服务器返回 304 Not Modified,则表示缓存未修改,任然可用,更新缓存的 header;
// 否则表示缓存过期,服务器会直接返回原始数据
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
最后,如果响应可以被缓存的话,保存缓存。
if (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}
至此,CacheInterceptor 的对缓存的处理流程大致就分析完了,总之这个处理流程也是按照 Http 规范来执行的,具体的缓存处理流程可以参考《HTTP 权威指南》第七章,其中还讲解了如何计算缓存的存活时间、缓存的新鲜时间等。
网友评论