要理解CacheInterceptor,需要对http协议请求头和响应头有些了解
响应头 | 说明 | 示例 |
---|---|---|
Date | 消息发送的时间 | Date: Sat, 18 Nov 2028 06:17:41 GMT |
Expires | 资源过期的时间 | Expires: Sat, 18 Nov 2028 06:17:41 GMT |
Last-Modified | 资源最后修改时间 | Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT |
ETag | 资源在服务器的唯一标识 | ETag: "16df0-5383097a03d40" |
Age | 服务器用缓存响应请求,该缓存从产生到现在经过 多长时间(秒) | Age: 3825683 |
Cache-Control | 请求头带Cache-Control:no-cache表示客户端不打算使用缓存,响应头带Cache-Control:no-cache表示不允许客户端使用缓存 | Cache-Control:no-cache |
请求头 | 说明 | 示例 |
---|---|---|
If-Modify-Since | 和Last-Modified关联使用,服务器下发一次资源,会同时下发资源的最后修改时间(Last-Modified),当客户端再次请求这个资源时,把上一次保存的最后修改时间带给服务器(通过请求头If-Modify-Since把Last-Modified带给服务器),服务器会把这个时间与服务器上实际文件的最后修改时间进行比较。如果时间一致,那么返回HTTP状态码304(不返回文件内容),客户端接到之后,就直接使用本地缓存文件;如果时间不一致,就返回HTTP状态码200和新的文件内容,客户端接到之后,会丢弃旧文件,把新文件缓存起来 | If-Modified-Since: Fri, 22 Jul 2016 源,返回304(无修改) 02:57:17 GMT |
If-None-Match | 和Etag关联使用,第一次发起http请求资源时,服务器会返回一个Etag(假设Etag:abcdefg1234567),在第二次发起同一个请求时,客户端在请求头同时发送一个If-None-Match,而它的值就是Etag的值(If-None-Match:abcdefg1234567),这个请求头需要客户端自己设置。然后服务器收到后会对比客户端发送过来的Etag是否与服务器的相同,如果相同,就将If-None-Match的值设为false,返回状态为304,客户端继续使用本地缓存;如果不相同,就将If-None-Match的值设为true,返回状态为200,客户端重新解析服务器返回的数据 | If-None-Match:abcdefg1234567 |
Cache-Control | 可以在请求头存在,也能在响应头存在 | 1. max-age=[秒] :资源最大有效时间;2. public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源; 3. private :表明该资源只能被单个用户缓存,默认是private。4. no-store :资源不允许被缓存5. no-cache :(请求)不使用缓存6. immutable :(响应)资源不会改变7. min-fresh=[秒] :(请求)缓存最小新鲜度(用户认为这个缓存有效的时长)8. must-revalidate :(响应)不允许使用过期缓存9. max-stale=[秒] :(请求)缓存过期后多久内仍然有效 |
流程
Cacheinterceptor的核心在CacheStrategy类中,他会根据CacheStrategy对象中得到的networkRequest和cacheResponse两个成员的情况来判断是使用缓存还是请求服务器,判断关系如下表格
networkRequest | cacheResponse | 说明 |
---|---|---|
Null | Not Null | 直接使用缓存 |
Not Null | Null | 向服务器发起请求 |
Null | Null | okhttp返回504 |
Not Null | Not Null | 发起请求,若得到响应为304(无修改),则更新缓存响应并返回 |
那么具体是怎么判断的?我们进入类中看看源码
ini
CacheStrategy strategy =
new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
在new Factory时如果缓存存在,就解析缓存中的响应头,保存在类中备用
arduino
public Factory(long nowMillis, Request request, Response cacheResponse) {
....//Date Expires Last-Modified ETag Age
}
然后调用get方法
csharp
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
//todo 如果可以使用缓存,那networkRequest必定为null;指定了只使用缓存但是networkRequest又不为null,冲突。那就gg(拦截器返回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;
}
后边的核心逻辑都在getCandidate方法中,根据情况返回相应的CacheStrategy
1.首先判断有没有缓存
cacheResponse 是从缓存中找到的响应,如果为null,那就表示没有找到对应的缓存,创建的 CacheStrategy 实例 对象只存在 networkRequest ,这代表了需要发起网络请求
csharp
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
2.判断如果是https请求,有没有握手信息
如果本次请求是HTTPS,但是缓存中没有对应的握手信息,那么缓存无效。okhttp会保存ssl握手信息 Handshake ,如果这次发起了https请求,但是缓存的响应中没有握手信息,则需要发起网络请求
vbscript
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
3.判断响应码和响应头是否满足使用缓存的要求
vbscript
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
4.判断用户设置
先对用户本次发起的 Request 进行判定,如果用户指定了 Cache-Control: no-cache (不使用缓存)的请求头或者请求头包含 If-Modified-Since 或 If-None-Match (请求验证),那么就不允许直接使用缓存,而是需要询问服务器。这意味着如果用户请求头中包含了这些内容,那就必须发起请求。如果服务器返回304,那么就可以使用缓存
scss
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
5.判断资源是否不变
缓存是上一次服务器下发的资源,在这个下发中,判断响应头是否存在Cache-Control:immutable,如果存在,那么说明服务器告诉我们这个资源不会改变,那就可以直接使用缓存
ini
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
6.判断缓存是否过期
ini
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());
}
7.缓存过期后的处理
ini
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.
}
//todo 如果设置了 If-None-Match/If-Modified-Since 服务器是可能返回304(无修改)的,使用缓存的响应体
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);
总结
1、如果从缓存获取的 Response 是null,那就需要使用网络请求获取响应; 2、如果是Https请求,但是又丢失了 握手信息,那也不能使用缓存,需要进行网络请求; 3、如果判断响应码不能缓存且响应头有 no-store 标识,那 就需要进行网络请求; 4、如果请求头有 no-cache 标识或者有 If-Modified-Since/If-None-Match ,那么需要进行 网络请求; 5、如果响应头没有 no-cache 标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行 网络请求; 6、如果缓存过期了,判断响应头是否设置 Etag/Last-Modified/Date ,没有那就直接使用网络请求否 则需要考虑服务器返回304;
并且,只要需要进行网络请求,请求头中就不能包含 only-if-cached ,否则框架直接返回504!