缓存知识
在开始Okhttp的缓存策略前,先了解HTTP权威指南中PC端和服务器之间的一些缓存知识 PC端缓存既可以是本地缓存,也可以是代理服务器上的缓存。但Okhttp中的缓存是指本地缓存,因为代理缓存也进行网络请求了,没有区分代理缓存还是服务器上的需求。 缓存命中和未命中:如果有缓存就叫缓存命中,同理没有缓存即只能通过源端服务器叫缓存未命中。 再验证:源端服务器的内容可能会发生变化,但缓存内容是一直不变,这时就需要再验证判断缓存是否过期,而再验证的操作不可能随时去检测,所以时机是客户端发起请求时。
再验证
再验证的两种方式:
1、Last-Modified和If-Modified-Since配合:获取到服务器响应后,客户端保存缓存时也保存了响应中的Last-Modified首部值,表示该内容最后的修改时间,如果缓存过期,会判断缓存中是否有Last-Modified首部,有则发起一个HEAD请求,携带一个If-Modified-Since首部,值是Last-Modified的值,服务器接收到会将现在内容的修改时间和传过来的比较,如果现在内容的修改时间比缓存中的大,说明修改了,即再验证未命中,再验证未命中结果上面说过,不然就是等于了就是再验证命中结果,客户端继续用缓存。
2、ETag和If-None-Match(优先级高于Last-Modified和If-Modified-Since)
获取到服务器响应后,客户端保存缓存时也保存了响应中的ETa首部值,ETa表示客户端当前资源在服务器的唯一标识(生成规则由服务器决定)。如果缓存过期,会判断缓存中是否有ETa首部,有则发起一个HEAD请求,携带一个If-None-Match首部,通过此字段通知服务器客户端缓存数据的唯一标识。服务器收到请求后发现有头部If-None-Match则与被请求的资源的唯一标识进行对比,不同则说明资源被改过,即再验证未命中结果,相同则说明资源没有被改动过,即再验证命中结果,客户端继续用缓存。
总结:只要请求时有If-Modified-Since或If-None-Match首部,就属于再验证,而请求方法为GET,其他方法不建议使用这个首部,因为本身Okhttp就只会缓存GET响应,所以其他请求方法没必要使用该首部,而且再验证也不能使用HEAD方法,因为要是再验证未命中需要返回新的body,而HEAD不返回body的。
那服务器对再验证请求有哪些响应:
1、再验证命中:源端服务器的内容没修改,即和缓存内容一样,响应是HTTP 304 NOT Modified,注意这时没有body的
2、再验证未命中:源端服务器的内容和缓存不同,服务器向客户端发送一条普通的、带有完整内容的HTTP 200 OK响应
3、源端服务器的内容被删除:服务器返回404 NOT Found响应,缓存也会被删除
再验证请求和原来请求的区别:就是有无If-Modified-Since或If-None-Match首部,表现不同的地方是再验证请求命中缓存时会返回304。
注意虽然有时添加了再验证首部,而且源端服务器的内容没改变,但有的源端服务器并没有对再验证首部做出304反应,而是200返回一样的内容。
判断缓存是否过期可以判断当时响应的Expires(绝对时间)或Cache-Control:max-age=xx(响应生成过去的时间),但实际上计算比这个复杂多了,具体看下面Okhttp中的处理。
注意我们在请求时如果Cache-Control:no cache,就会进行强制再验证,意思就是即使缓存没有过期,也会去再验证,如果再验证命中就使用缓存,如果再验证未命中就去返回源端服务器内容。
Okhttp的Cache-Control
Cache-Control 是缓存时最重要的首部。常见的取值有private、public、no-cache、max-age、no-store、默认是private。
针对Okhttp里 Cache-Control值的作用
但注意Cache-Control在请求和响应时都可以使用该首部,有些值能公用,有些值只有请求特有的,同理响应特有值
下面Cache-Control值的意义都是来自Okhttp的处理
请求时Cache-Control的值的意义:
no-cache:即使有缓存,即使缓存未过期,也会向服务器发起原始请求
no-store:缓存应该尽快被删除,避免敏感数据泄露,但Okhttp中并没有因为请求有no-store而去删除缓存,如果请求是no-store,向服务器发起原始请求。
max-age:s:如果本地或代理服务器的缓存时间大于s秒,就不会获取缓存,除非同时发送了max-stale:可以随意提供过期的文件,如果加max-stale:s,最长可以过期s秒。
only-if-cached(请求特有):不使用网络,缓存过期再验证或原始请求会返回504,未过期缓存返回缓存
max-stale(请求特有):接受过期时间,即缓存保质期是max-age加max-stale
min-fresh(请求特有):缓存的资源至少要保持指定时间的新鲜期,和max-stale意思相反,缓存保质期是max-age减min-fresh
比如第一次响应是是4月5日缓存, 且在4月12日过期(max-age=7),再次请求时max-age=10 days, max-stale=2days, min-fresh=3 days,那到底哪天算过期呢,
max-age=10对应4.5+10,即4.15过期
max-stale=2时到底是从4.12还是4.15开始呢,是以缓存中的过期时间算,即4.12算过期,加上max-stale的2是4.14算过期
min-fresh=3表示至少要留有3天的新鲜期, 缓存资源将在4月9日失效(12-3=9)。
这样看起来有三个日期,但因为总是采用最保守的缓存策略,,所以4.9发起请求时会再验证
响应时 Cache-Control的值的意义 :
no-cache:响应可以被缓存,但是每次请求如果满足再验证条件就原始请求添加再验证首部,否则不添加,再验证结果上面说过,成功就304,失败就200。这样看起来和请求时的no-cache不是一个意思,请求的no-cache会直接原始请求,而缓存的no-cache每次强制再验证
no-store: 响应不会被代理服务器缓存,代理服务器会转发no-store,但我觉得本地可以缓存,不然本地就没缓存,就没必要判断有没有no-store,Okhttp的确也是这样处理的,不管是put(第一次缓存)还是update(更新缓存),只要是GET请求就会去缓存,不管Cache-Control的值是什么,而再次请求时是不会用缓存的,是直接网络请求,获取到响应会去更新,但从来不用,所以no-store的响应就没必要缓存,虽然Okhttp是默认缓存的。
响应时的no-store和请求时的no-store一样,并不代表没有缓存,而是不会使用缓存,所以不会先进行再验证,直接去网络请求。而且只要任意一个有no-store就直接去网络请求,不使用缓存。
max-age:s:s秒之后缓存过期,那看起来和请求时是一样的意思,不过一个是响应时给定的过期时间,请求时的max-age是我们自定义过期时间,但两者取最小值。
must-revalidate(响应特有):可缓存但必须再向源服务器进行确认,这样看起来是直接每次原始请求,而事实上在Okhttp中的处理并不是这样的,他只有一个作用,那就是使请求的max-stale失效,但可能缓存未过期
注意:如果某个值是请求特有的,那么给响应添加是没有任何效果的,反之同理。
比如我响应中添加max-stale是没有用的,因为计算缓存时max-stale是从请求中查找的
CacheInterceptor
缓存策略总结
Okhttp中的缓存指本地缓存,策略实现就是靠本地缓存和Cache-Control结合实现的
服务器返回200那里,决定是否缓存不止受no-store影响,也有有些状态码本身不支持缓存,看iCacheStrategy的sCacheable(),而且只支持GET缓存,具体看Cache的put()
注意缓存的update()操作只有在返回304,才算update(),更新请求和响应时间,而再验证失败返回新的内容,这个是属于和第一次缓存一样,都是put操作。
上图共5种情况
情况1:无本地缓存
情况2:上面这四种情况,但注意第4点,可能有再验证首部,这样就可能返回304使用本地缓存
情况3:缓存过期,但没有Etag,LastModified,还是用原始请求
情况4:缓存过期,有Etag或LastModified,原始请求添加If-Modified-Since或If-None-Match
情况5:缓存未过期
缓存策略通过CacheStrategy类实现,最终的缓存策略是由CacheStrategy的networkRequest和cacheResponse决定
//原始请求,如果为null就不进行网络请求
public final @Nullable Request networkRequest;
//缓存,如果为null,就表示没有该缓存或者不使用缓存
public final @Nullable Response cacheResponse;
| networkRequest | cacheResponse | 场景和结果 |
|---|---|---|
| null | null | 情况1,2,3,4有only-if-cached,返回504空响应;情况5返回本地缓存 |
| null | not-null | 情况5返回本地缓存 |
| not-null | null | 情况1,2,3没有only-if-cached,返回原始请求响应并缓存 |
| not-null | not-null | 情况4 返回304使用本地缓存,返回200结果并缓存 |
注意事项
1、请求和响应中谨慎使用no-store,会导致无法使用本地缓存,除非请求手动添加再验证首部
2、请求和响应的no-cache效果不同,前者是直接原始请求,后者不会去计算缓存是否过期,可能会添加再验证首部
3、请求谨慎使用only-if-cached,这个是不使用网络,所以除非本地缓存未过期,其他情况(进行网络请求)都是504
源码简析
CacheInterceptor中根据当前Request和缓存cacheCandidate去实例化CacheStrategy,get()中决定networkRequest和cacheResponse的值,从而影响真正的缓存策略
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
//networkRequest!=null,可能是添加再验证的原始请求也有可能是默认的原始请求,
//only-if-cached是不使用网络,所以冲突,结果是504空响应
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() {
//无本地缓存,情况1
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
//情况2.1:如果Https请求且丢失了握手,就将cacheResponse置为null,为啥?缓存body可能还在呀
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.
//情况2.2:通过请求和本地的缓存响应是否有no-store,有一个是就不使用缓存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//Cache-Control是no-catch或当前请求首部里有再验证的两个首部就不使用缓存
//情况2.3和情况2.4:前者是强制不使用缓存,后者是缓存到期了去再验证
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//计算缓存年龄,具体看下面,来自RFC 2616, 13.2.3 Age Calculations
long ageMillis = cacheResponseAge();
//获取缓存保鲜时间,具体看下面
long freshMillis = computeFreshnessLifetime();
//取请求的max-age时间和缓存响应新鲜值的最小值,因为两者都代表新鲜时间,request是我们要求的
//response是服务器要求的,取小的保证缓存肯定新鲜
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
//获取当前Request的Cache—Control的min-fresh的值,
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
//缓存响应中有must-revalidate时请求时会每次去确认,所以要请求的max-stale有意义必须没有must-revalidate
CacheControl responseCaching = cacheResponse.cacheControl();
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//计算是否过期前提是缓存没有no-cache
//没过期:ageMillis(响应报文的年龄)+min-fresh<最小的保鲜期+允许过期的时间
//没过期就request为null,不进行网络请求
//情况5
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
//说明过期但还在max-stale内
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
//缓存超过一天且没有指定保鲜日期(即response没有max-age和expires),就给个警告
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 {
//情况3:没有再验证条件就去网络请求
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
//情况4:原始请求添加再验证首部
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
isCacheable总结:下面提到的200等这些状态码是默认支持缓存,但最后真正是否能做为缓存还是得看当前Request和缓存的response的Cache-Control有没有no-store,有一个是no-sotre就不支持缓存。其他状态码不支持缓存,直接默认false。
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()) {
//200
case HTTP_OK:
//203,内容是代理服务器的缓存,但没有Authorization验证
case HTTP_NOT_AUTHORITATIVE:
//204,没有body
case HTTP_NO_CONTENT:
//300,重定向多个url
case HTTP_MULT_CHOICE:
//301,永久重定向
case HTTP_MOVED_PERM:
//404,无法找到url的资源
case HTTP_NOT_FOUND:
//405,方法不在url请求的方法内
case HTTP_BAD_METHOD:
//410,资源没了,类似404,但曾经拥有,服务器可以通知客户端
case HTTP_GONE:
//414,url太长
case HTTP_REQ_TOO_LONG:
//501,服务器遇到错误
case HTTP_NOT_IMPLEMENTED:
//308,永久重定向
case StatusLine.HTTP_PERM_REDIRECT:
// These codes can be cached unless headers forbid it.
//上面这些虽然没返回真正内容,但是默认是允许缓存的
break;
//302,临时重定向
case HTTP_MOVED_TEMP:
//307,临时重定向,只能get重定向
case StatusLine.HTTP_TEMP_REDIRECT:
// 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.
//302或307的缓存必须没有Expires和max-age和非public和非private,否则直接false,不使用缓存
//但是默认不是private的吗,注意这并不代表有缓存,只能说如果有缓存是默认是私有缓存
//那意思是临时重定向的缓存不能拿来用?
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.
//缓存有noStore和请求有noStore有一个满足就返回false,说明no-store就是不用缓存
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
注意:要实现缓存,必须client调用cache()或setInternalCache(),前者优先级高,而且前者InternalCache接口在Cache中有默认实现,该接口是CacheInterceptor和Cache的增删改查的连接器;如果是setInternalCache(),就必须自己实现InternalCache接口的所有方法,即对缓存的增删改查。如果两者都没设置,cache(InternalCache)就会为null,就没有缓存功能了。 如果是设置cache(),那么InternalCache在Cache中有个默认实现,通过internalCache回调Cache中的增删改查方法,代码就不贴出了。
现在再来看下CacheInterceptor
public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}
@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;
//统计策略,如果networkRequest!=null,说明网络请求,Cache的networkCount++,
//同理cacheResponse!=null对应Cache的hitCount++
if (cache != null) {
cache.trackResponse(strategy);
}
//如果cacheResponse为null,即不使用缓存,就关闭缓存cacheCandidate
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(EMPTY_BODY)
.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 {
//执行到这里networkRequest不为null,但cacheResponse不一定为null,null说明不使用缓存
//不为null是缓存过期但添加再验证
//网络请求了
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
//响应没有body,但不使用缓存,就关闭缓存
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
//cacheResponse不为null说明是有再验证首部的原始请求
if (cacheResponse != null) {
//状态码是304说明缓存没被改变,所以还是用cacheResponse
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
//以缓存响应为基础构建一个新的response
Response response = cacheResponse.newBuilder()
//合并cacheResponse和networkResponse的首部
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
//更新请求时间为再验证请求的时间
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
//更新响应时间为再验证响应时间
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
//清空cacheResponse和networkResponse的body,不知道为什么清除,但缓存的body
//已经通过newBuilder()已经传递给response了
.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统计hitCount++
cache.trackConditionalCacheHit();
//更新缓存cacheResponse为现在的合并好的缓存response
cache.update(cacheResponse, response);
return response;
} else {
//说明不是304,即缓存发生改变,关闭缓存
closeQuietly(cacheResponse.body());
}
}
//执行到这说明没有缓存或缓存过期再验证未命中,返回新内容
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//cache!=null,说明开启缓存的两种方法至少有一种
if (cache != null) {
//前者是判断响应有无body,isCacheable也讲过,即如果响应和请求没有no-store,有的状态
//码即使没有no-store也返回false,即不支持缓存,才去缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
//第一次缓存写入本地
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//判断请求如果是"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"中的任何一个则
//调用DiskLruCache.remove(urlToKey(request));将这个请求从缓存中移除出去。
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
计算缓存是否过期
计算缓存年龄
servedDate: 报文产生时间,对应响应的Date首部
receivedResponseMillis: 接收到响应,即构建Response的时间,为当前时间,如果是缓存再验证,不管成功与否,都更新receivedResponseMillis为当前时间;如果使用的是本地缓存,这个值还是上次网络请求的response的值。
sentRequestMillis:发起网络请求的时间,同上,只要进行网络请求,就会更新;用的本地缓存就还是上次的;504就是-1。
private long cacheResponseAge() {
//receivedResponseMillis
long apparentReceivedAge = servedDate != null
? Math.max(0, receivedResponseMillis - servedDate.getTime())
: 0;
long receivedAge = ageSeconds != -1
? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
: apparentReceivedAge;
long responseDuration = receivedResponseMillis - sentRequestMillis;
long residentDuration = nowMillis - receivedResponseMillis;
return receivedAge + responseDuration + residentDuration;
}
我们说过缓存是有本地缓存和代理服务器缓存的,缓存年龄就是从响应报文时间(servedDate)那刻开始计算,而且不管是代理服务器缓存还是直接从源端服务器取,响应报文时间是不变的,都是第一次产生响应的时间。
所以注意如果是代理服务器的缓存,servedDate要比发送请求时间早得多,如果是从源端服务器获取,源端服务器在接收到请求后产生servedDate。
在HTTP1.1时,如果从代理服务器获取到缓存,响应有个Age首部(响应年龄),=响应时的时间-响应报文产生时间,帮我们计算好了,但是HTTP1.1之前没有Age,只能自己计算,也是响应时的时间-响应报文产生时间,就是上面的apparentReceivedAge。所以为了适配所有情况,先计算apparentReceivedAge,如果有Age首部,说明是HTTP1.1取age和apparentReceivedAge的最大值,可能有时差误差,取大的。如果没有Age,那就用apparentReceivedAge
直接源端服务器获取的响应
获取的是代理服务器的缓存的年龄
response到nowMills表示获取响应后本地缓存的年龄
不管哪种方式,代码中计算缓存年龄总是receivedAge + responseDuration + residentDuration
主要serverDate到nowMills这段时间占大头
不过这样算的话,对比图的话,不知道为什么,都重复多算了一段时间。。。
缓存保鲜时间
缓存保鲜时间,优先是缓存响应的max-age指定的时间,没有的话是expires(绝对日期)减报文产生日期,
如果两种都没设置,那么是(某次响应中返回的lastmodify时间-报文产生日期)/10,不太懂。
注意,如果computeFreshnessLifetime()返回0(即响应没有max-age或expires)会导致即使设置了请求的max-age还是0,所以在响应中修改max-age
private long computeFreshnessLifetime() {
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.maxAgeSeconds() != -1) {
return SECONDS.toMillis(responseCaching.maxAgeSeconds());
} else if (expires != null) {
long servedMillis = servedDate != null
? servedDate.getTime()
: receivedResponseMillis;
long delta = expires.getTime() - servedMillis;
return delta > 0 ? delta : 0;
} else if (lastModified != null
&& cacheResponse.request().url().query() == null) {
// As recommended by the HTTP RFC and implemented in Firefox, the
// max age of a document should be defaulted to 10% of the
// document's age at the time it was served. Default expiration
// dates aren't used for URIs containing a query.
long servedMillis = servedDate != null
? servedDate.getTime()
: sentRequestMillis;
long delta = servedMillis - lastModified.getTime();
return delta > 0 ? (delta / 10) : 0;
}
return 0;
}
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
计算缓存是否过期
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());
}
注意响应不能有no-cache,否则直接再验证,不管缓存是否过期
ageMillis + minFreshMillis < freshMillis + maxStaleMillis
缓存年龄+min-fresh<缓存保鲜时间+max-stale
缓存年龄上面我们计算过,最好不要去修改。304时更新响应时间和请求时间,相当于更新了ageMillis,但其实感觉影响也不大,now-serverData占大头,serverData没变。
我们可以改变请求时的min-fresh来提前使缓存过期,没必要使用负值,那就是max-stale的意思了,其实min-fresh也用到不多,多数是直接不使用缓存,min-fresh可以值很大,但还不如请求时no-cache或no-store,直接原始请求
CacheControl中的FORCE_NETWORK就是添加了no-cache,也可以自己添加addHead(Cache-Control,"no-cache")
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
request=request.newBuilder()
.cacheControl(CacheControl.FORCE_NETWORK)
.build()
我们可以修改请求时的max-stale来延时缓存过期,注意must-revalidate会使max-stale失效,当我们只想用本地缓存时,使用only-if-cache,但如果有网络请求(缓存过期再验证的原始请求或原始请求)都会返回504,那就必须走情况5,让缓存不过期,给max-stale设置无限大,但注意要走到情况5,必须不能满足情况2,即不能有no-cache或no-store。CacheControl类来提供Cache-Control的值,其内部的FORCE_CACHE就是只使用本地缓存。
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
实战缓存
通过internalCache的trackConditionalCacheHit()和trackResponse()回调来判断到底执行了什么操作
requestCount++:只要执行到缓存拦截器就会累加,即只要发起请求,不管走不走网络请求就会累加,因为重试机制可能一次请求会多次增加
networkCount++:缓存策略的网络请求不为null,情况上面说过,可能是缓存过期原始请求添加再验证,也可能是直接原始请求,总之就是进行网络请求了
hitCount++:有两处,一是缓存策略的网络请求为null,缓存不为null,就是缓存未过期;二是304更新了缓存。
但是这两种情况是互斥的,一次请求不可能加2,即每次加1
而networkCount和hitCount也是互斥的,requestCount=networkCount+hitCount
再次请求,只要cache()里Cache实例不变,那么上面几个值是累加的,虽然拦截器都实例化是新的了。
需求1
没有网络时只使用本地缓存,有网络时90s内使用本地缓存,这个时间根据实际情况调整,要是数据后台频繁刷新那就调小点,反之大点
class MainActivity1 : AppCompatActivity() {
val mCachePath = File(AppContext.sContext.externalCacheDir, "cache")
val cache = Cache(mCachePath, 10 * 1024 * 1024)
lateinit var client: OkHttpClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//list可以重复元素,所以mCacheInterceptor执行了2次,但不影响
val mCacheInterceptor=MyCacheInterceptor(cache)
client = OkHttpClient.Builder().cookieJar(MyCookieJar())
.cache(cache)
.addInterceptor(mCacheInterceptor)
.addNetworkInterceptor(mCacheInterceptor)
.build()
request_btn.setOnClickListener {
response_tv.text = ""
handle()
}
handle()
}
private fun handle() {
client.newCall(getRequest()).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d("sdfasfasfsf", "error=" + e.message)
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
Log.d("sdfasfasfsf", "responseCode=" + response.code())
val res = response.body()!!.string()
runOnUiThread { response_tv.text = res }
}
})
}
private fun getRequest() = Request.Builder()
.url("https://www.wanandroid.com/lg/collect/list/0/json")
.build()
}
class MyCacheInterceptor(val cache: Cache) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!isNetworkAvailable()) {
//无网络只使用本地没缓存,注意FORCE_CACHE是only-if-cache+很大的max-stale
// 只使用only-if-cache会容易再验证请求导致504
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
val response = chain.proceed(request)
Log.d("sdfasfasfsf", "request" + cache.requestCount())
Log.d("sdfasfasfsf", "NetRequest" + cache.networkCount())
Log.d("sdfasfasfsf", "hitCount" + cache.hitCount())
// if (!isNetworkAvailable()) {
//这种无网络设置max-age毫无意义,因为无网络时本身FORCE_CACHE通过max-stale
//使缓存不过期,再添加max-age多此一举
// val maxAge = 0
// return response.newBuilder()
// .removeHeader("Pragma")
// .header("Cache-Control", "max-age=$maxAge")
// .build()
//} else {
//如果请求的min-frresh和max-stale都不设置,那就是90s内保证缓存不过期,保证使用本地缓存
//上面FORCE_CACHE是上次的,不影响下次请求request,都实例化新request了
val maxAge = 90//90s
return response.newBuilder()
//HTTP/1.0 缓存可能没有实现 Cache-Control,并且也没有实现 Pragma: no-cache
//不知道什么用,兼容提示作用?
.removeHeader("Pragma")
//不用担心响应有no-cache或no-store,因为是先移除Cache-Control的所有值再添加新的
.header("Cache-Control", "max-age=$maxAge")
.build()
// }
return response
}
private fun isNetworkAvailable(): Boolean {
var mConnectivityManager: ConnectivityManager = AppContext.getContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val mNetworkInfo: NetworkInfo? = mConnectivityManager.getActiveNetworkInfo()
if (mNetworkInfo != null)
return mNetworkInfo?.isAvailable()
return false
}
}
class MyCookieJar : CookieJar {
var mCookies = mutableListOf<Cookie>()
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
mCookies.clear()
mCookies = cookies
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
if (mCookies.isEmpty()) {
val cookieUser = Cookie.Builder()
.name("loginUserName")
.value("xxx")
.domain("s")
.build()
val cookiePassword = Cookie.Builder()
.name("loginUserPassword")
.value("xxx")
.domain("a")
.build()
mCookies.add(cookieUser)
mCookies.add(cookiePassword)
}
return mCookies
}
}
代码中有些注释说过了,还需注意几点:
1、没有网络时只使用本地缓存时,那本地缓存都没有,那就只能由CacheInterceptor返回504了
2、设置max-age可以根据后台数据刷新时间调整,无网络时FORCE_CACHE已经设置max-stale很大就没必要设置max-age了。注意添加的是响应中的max-age,看过上面的缓存计算知道,有的时候响应没有max-age或expires,
请求的max-age就会失效,最终的max-age还是0。
3、如果想设置max-age必须添加addNetworkInterceptor(),因为addInterceptor()添加的拦截器获取的响应是CacheInterceptor处理后返回的,而进行网络请求后CacheInterceptor先是将响应缓存后(put或update)才返回,所以拦截器添加的max-age并没有添加到本地缓存中,下次读取本地缓存的max-age不是自己修改的。所以添加addNetworkInterceptor()后先将响应结果修改max-age,然后让CacheInterceptor保存或更新缓存。
4、上面例子的addInterceptor()和addNetworkInterceptor()用的是同一个类作为拦截器,其实应该各自一个更好,
addInterceptor专门处理请求前和最终返回结果
addNetworkInterceptor专门处理刚返回的响应
用同一个类,多执行了无意义代码,就比如max-age修改只在NetworkInterceptor中才有用。
分开写如下:
client = OkHttpClient.Builder().cookieJar(MyCookieJar())
.cache(cache)
.addInterceptor(RequestCacheInterceptor(cache))
.addNetworkInterceptor(ResponseCacheInterceptor())
.build()
class RequestCacheInterceptor(val cache: Cache) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!isNetworkAvailable()) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
val response=chain.proceed(request)
Log.d("sdfasfasfsf", "request" + cache.requestCount())
Log.d("sdfasfasfsf", "NetRequest" + cache.networkCount())
Log.d("sdfasfasfsf", "hitCount" + cache.hitCount())
return response
}
}
class ResponseCacheInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val maxAge = 30//30s
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=$maxAge")
.build()
return response
}
}
第一次请求打印结果:
request1
NetRequest1
hitCount0
30s内第二次请求打印结果:
request2
NetRequest1
hitCount1
30s(缓存过期)请求打印结果:
request4
NetRequest3
hitCount1
的确是网络请求,但为什么加2呢,说明缓存拦截器被重复执行,而重复执行除了我们自己主动调用chain.proceed(request)以外的唯一原因是请求失败,RetryAndFollowUpInterceptor重试重新执行一遍拦截器。
可以dubug发现RetryAndFollowUpInterceptor的第一次返回IO异常,至于为什么一定要重试第二遍有待研究。
无网络时请求打印结果
request5
NetRequest3
hitCount2
的确使用了缓存
需求2:
每次都进行原始网络请求,那就不要设置cache()了,或者设置cache()后请求带no-cache或no-store或响应带no-store
如果想每次强制再验证,这和上面的每次进行原始网络请求还是不同的,强制再验证是有缓存功能的,虽然每次再验证,但是如果再验证命中返回304就使用缓存,可比原始网络请求需要传递body快多了,这种适合数据时刻保持最新,只要响应添加no-cache即可,这种是和max-age是冲突的,所以写法上只要将需求1里的max-age替换成no-cache即可。至于无网络只使用缓存可以根据情况是否删掉。
return response.newBuilder()
//HTTP/1.0 缓存可能没有实现 Cache-Control,并且也没有实现 Pragma: no-cache
//不知道什么用,兼容提示作用?
.removeHeader("Pragma")
.header("Cache-Control", "no-cache")
.build()
需求3:
在1需求下,如果服务器返回异常,那么就去使用缓存。
思路:那肯定是在addInterceptor中操作,因为要再次去执行CacheInterceptor,这时为了使用本地缓存,那肯定是only-if-cache
这个没测试过
class RequestCacheInterceptor(val cache: Cache) : Interceptor {
lateinit var response: Response
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!isNetworkAvailable()) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
response=chain.proceed(request)
//随便写的,根据实际情况调整
if(response.code()==200){
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
//如果本地缓存都没有,那就只能返回504了
response=chain.proceed(request)
}
Log.d("sdfasfasfsf", "request" + cache.requestCount())
Log.d("sdfasfasfsf", "NetRequest" + cache.networkCount())
Log.d("sdfasfasfsf", "hitCount" + cache.hitCount())
return response
}
}
//不变
class ResponseCacheInterceptor: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val maxAge = 30//30s
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=$maxAge")
.build()
return response
}
}
假如按上面测试用200(异常情况跟着调整),也就是说返回200就去使用本地缓存,运行第一次就有缓存但过期:
request2
NetRequest1
hitCount1
刚开始网络请求NetRequest++,然后返回200再次请求但使用本地缓存,所以hitCount++。
总结
核心就是这张图