okhttp分享四:CacheInterceptor
在介绍okhttp缓存逻辑实现之前,首先我们介绍下http缓存相关知识
一、HTTP缓存介绍
1.1、 为什么要用缓存
1、减少请求次数,较少服务器压力
2、本地数据读取更快,让页面不会空白几百毫秒
3、在无网络的情况下提供数据 HTTP缓存是最好的减少客户端服务器往返次数的方案,缓存提供了一种机制来保证客户端或者代理能够存储一些东西,而这些东西将会在稍后的HTTP响应中用到的。(即第一次请求了,到了客户端,缓存起来,下次如果请求还需要一些资源,就不用到服务器去取了)这样,就不用让一些资源再次跨越整个网络了。
1.2、缓存分类
按照是否向直接服务器发起请求进行对比分类,可以分为两类,即强制缓存和对比缓存。
1、强制缓存
已存在缓存数据时,强制缓存由请求头中Cache-Control字段控制,仅基于强制缓存,请求数据流程如下:
2、对比缓存缓存
已存在缓存数据时,对比缓存由请求头中ETag/If-None-Match,Last-Modified/If-Modified-Since四个字段控制。仅基于对比缓存,请求数据流程如下:
3、整体流程
1.3、http协议强制缓存对应字段
- Cache-Control:no-cache
如果请求头中包含no-cache,表示客户端不会接受缓存的响应。需要把客户端请求转发给源服务器。 - Cache-Control:no-store
出现在响应头表示该响应不会被代理或客户端缓存。 - Cache-Control:private
出现在响应中,表示该响应只能被客户端缓存,代理不可以缓存。 - Cache-Control:public
出现在响应中,表示该响应可以被代理或客户端缓存。 - Cache-Control:no-transform
不论出现在请求还是响应中,代理服务器都不能改变实体主题的媒体类型,如.jpg不可以改变成.png,不能压缩解压缩等。 - Cache-Control:max-age (秒)
在请求头中出现,表示若缓存存在时间小于该值则直接返回,不会请求网络。
在响应头中表示该资源的有效期。 - Cache-Control:max-stale
一般出现在请求头中,都指示客户端可以接收过期响应消息,如果指定max-stale消息的值,那么客户端可以接收过期但在指定值之内的响应消息,如果未指定该参数,表示无论过期多久都会采用响应。如max-age为300,max-stale为200,则缓存的过期时间为300秒,但是在300+200=500秒内仍然可以使用,但是此时在300-500秒内返回响应会带有状态码为110的警告,当超过500秒时,缓存不可用。 - Cache-Control:min-fresh
最小需要保留的新鲜度,只出现在请求头中。如max-age为300,max-stale为200,min-fresh为100则缓存的过期时间为300秒,但是在300+200-100=400秒内仍然可以使用。 - Cache-Control:only-if-cached,表示只用本地缓存,不管是否缓存过期。
Pragma:http1.0所用的字段,1.1中对应Cache-Control
Expired:http1.0所用头部,出现在响应头部,表示该实体何时过期,为绝对时间,功能类似max-age,但max-age为相对时间。 - Cache-Control:must-revalidate
只会出现在响应中,有两个作用1、本地判断响应是否过期时,max-stale不可用。2、若判断出本地缓存过期,需要向服务端发起请求,则代理服务器必须将请求向源服务器验证。若代理服务器无法联通原服务器,则会返给客户端504状态码,且此时缓存不可过期,即max-stale字段无效。(ok中该字段仅用于判断max-stale是否有用,一般使用配合max-age=0) - Cache-Control:immutable
表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-Match或If-Modified-Since)来检查更新,即使用户显式地刷新页面。(消灭304,ok中存在该字段但还未具体使用)。 - Age
响应头中出现,当代理服务器用自己缓存的实体去响应请求时,用此字段表示该实体从产生到现在经过多长时间,用于计算响应新鲜度,相对时间。 - Date
响应头中出现,表示源服务器产生该响应的时间,用于计算响应新鲜度,绝对时间。
1.4、http协议对比缓存对应字段
-
Last-Modified/If-Modified-Since
用户首次请求服务器时,服务器响应请求时,通过Last-Modified告诉客户端资源最后修改时间,客户端再次请求服务器时,通过If-Modified-Since告诉服务器,服务器收到后将该数据与资源最后修改时间对比,若资源的最后修改时间大于If-Modified-Since,说明资源被修改过,则重新返回响应,响应码为200,此时响应有实体;若果资源的最后修改时间小于或等于If-Modified-Since,说明资源未被修改,返回304告诉客户端继续使用缓存,注意此时响应只有头部,没有实体。
-
ETag/If-None-Match(优先级高于Last-Modified)
服务响应请求时,告诉客户端当前资源在服务器的唯一标识ETag(生成规则由服务器决定)。再次请求服务器时,通过If-None-Match字段通知服务器客户端缓存数据的唯一标识。服务器收到请求后发现有头部则与被请求的资源的唯一标识进行对比,不同则说明资源被改过,则响应整个内容,返回状态码是200,相同则说明资源没有被改动过,则响应状态码304,告知客户端可以使用缓存,注意此时响应只有头部,没有实体。
1.5、HTTP请求的幂等性
HTTP方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。说白了就是,同一个请求,发送一次和发送N次效果是一样的。
各个方法中,get、head、delete、option、put是幂等的,connect、methods、post、patch是非幂等的。我们主要关注get和post。若按照RFC协议规范,对于仅是查询,不改变服务器资源的请求应当使用get,而要改变服务器资源的请求应当使用post,与此对应Http协议的缓存以及重定向主要都是针对get的。
二、CacheControl,CacheStrategy,CacheInterceptor
okhttp实现缓存主要依靠CacheControl,CacheStrategy,CacheInterceptor三个类
http缓存逻辑实现大致分为三种
1、本地无缓存命中,直接走网络
2、有缓存命中且未过期,直接返回
3、有缓存命中,但缓存过期,走网络
接下来我们来分析代码
@Override 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 (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
介绍逻辑前先解释下几个参数
Response cacheCandidate:根据用户请求从本地缓存中取出的对应缓存响应(不一定使用),为空表示客户端未分配缓存。
CacheStrategy strategy:缓存策略,包含一个网络请求networkRequest(为空时表示不需要网络请求,不为空表示需要发送网络请求),cacheResponse(为空表示cacheCandidate不符合条件,不使用)。下面开始解释代码逻辑
- 1、首先获取cacheCandidate,并根据request、cacheCandidate获取CacheStrategy,并获取networkRequest,cacheResponse
- 2、若cacheCandidate不为空,但CacheStrategy中的cacheResponse为空,表示缓存不符合使用条件,则关闭缓存。
- 3、若networkRequest为空且cacheResponse也为空,则表示不可以使用网络但缓存也为空,则返回504
- 4、若networkRequest为null(此时cacheResponse不为null),说明不需要走网络,直接使用缓存响应,即我们上面说的强制缓存。
- 5、若4不满足,说明需要走网络验证,则调用chain.proceed方法进入下一个拦截器,走网络流程获取网络响应networkReponse
- 6、判断网络响应code是否为304,若为304则表示本地缓存未过期,可以使用,则更新cacheResponse的头部信息生成最终的response返回。
- 7、code不为304
- 7.1、直接根据netWorkResponse生成最终的response。
- 7.2、若用户配置了缓存地址
- 7.2.1、response有响应体、且response及其对应请求符合缓存条件(isCacheable())则将响应写入缓存
- 7.2.2、过滤缓存中响应,这些响应的请求方法不符合要求(invalidatesCache())
- 返回response
大致流程说完后,我们再来看下流程第2步中CacheStrategy的生成过程CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
,
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
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);
}
}
}
}
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
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;
}
可以看出,其初始化方法主要是给一些字段赋值,具体参数意义之前介绍字段时已经说过,这边就不再赘述。get方法实际上是调用了getCandidate()方法返回一个CacheStrategy,之后判断若CacheStrategy中的networkRequest不为null但请求配置为onlyIfCached,则说明用户配置只能使用缓存但缓存并不满足条件,因此更新CacheStrategy,将其networkRequest及cacheResponse都置空。
private CacheStrategy getCandidate() {
// No cached response.
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 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 new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
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;
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());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
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;
} 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);
}
···
public static boolean isCacheable(Response response, Request request) {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1),
// This implementation doesn't support caching partial content.
switch (response.code()) {
case HTTP_OK://200
case HTTP_NOT_AUTHORITATIVE://203
case HTTP_NO_CONTENT://204
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_NOT_FOUND://404
case HTTP_BAD_METHOD://405
case HTTP_GONE://410
case HTTP_REQ_TOO_LONG://414
case HTTP_NOT_IMPLEMENTED://501
case StatusLine.HTTP_PERM_REDIRECT://308
// These codes can be cached unless headers forbid it.
break;
case HTTP_MOVED_TEMP://302
case StatusLine.HTTP_TEMP_REDIRECT://307
// These codes can only be cached with the right response headers.
// http://tools.ietf.org/html/rfc7234#section-3
// s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
if (response.header("Expires") != null
|| response.cacheControl().maxAgeSeconds() != -1
|| response.cacheControl().isPublic()
|| response.cacheControl().isPrivate()) {
break;
}
// Fall-through.
default:
// All other codes cannot be cached.
return false;
}
// A 'no-store' directive on request or response prevents the response from being cached.
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
getCandidate方法是根据传入的当前请求request判断,缓存响应cacheResponse是否过期,其逻辑如下
- 1、若cacheResponse为空,则说明要走网络,则返回CacheStrategy(request, null)
- 2、若request为https,但cacheResponse中的握手信息丢失,则必须进行网络请求,返回CacheStrategy(request, null)
- 3、根据cacheResponse,request判断缓存响应是否可用,若不可用则返回CacheStrategy(request, null)。判断的方法为isCacheable(),简单说下:首先判断缓存响应的状态码是否符合要求,若符合要求在判断请求头、响应头中是否有noStore字段,若没有则说明可用。
- 4、获取请求头的cacheControl域requestCaching,若其中包含noCache、If-Modified-Since或If-None-Match则说明需要请求网络。返回CacheStrategy(request, null),这里很多人会奇怪,If-Modified-Since、If-None-Match表示对比缓存,若返回304则说明需要使用本地缓存,但这里为什么返回的cacheReponse为null呢。我们看下判断使用的方法
hasConditions(Request request)
,注释解释的很清楚,因为304针对的是响应头中包含If-Modified-Since、If-None-Match的情况,如果请求中出现了这两个字段,说明是用户自己加入的策略条件,因此不符合304使用场景,所以本地缓存将不使用。
/**
* Returns true if the request contains conditions that
save the server from sending a response
* that the client has locally. When a request is enqueued with its own conditions, the built-in
* response cache won't be used.
*/
private static boolean hasConditions(Request request) {
return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}
- 5、取出响应头中的cacheControl域requestCaching,若responseCaching中包含immutable字段,则直接返回CacheStrategy(null, cacheResponse)
- 6、计算响应已存在时间、响应过期时间,若响应过期时间>响应已存在时间,说明响应未过期,直接使用响应,返回CacheStrategy(null, builder.build())。
- 7、若6不满足,说明响应本地过期,需要发送网络请求验证缓存是否可用。若响应头中存在Etag、Last-Modified等信息(注意优先级),则加入请求头,生成conditionalRequest,返回CacheStrategy(conditionalRequest, cacheResponse)
大致逻辑已经讲完,整理了一张图,供大家参考