okhttp提供了让我们实现缓存的方法和接口,这部分的处理是发生在CaCheInterceptor拦截器中的。
这里说的缓存其实就是把服务器穿回来的响应存储在本地(可以是内存也可以在磁盘),当我们再次准备向服务器发送请求时,就可以直接获取缓存中的响应,而不必再次向服务器发出网络请求。
要完全理解CacheInterceptor的工作机制,其实还是得对Http有清晰的认识。文章最后,也会简单介绍下相关的知识。
1.自定义的缓存
CacheInterceptor的缓存实现是需要我们自己完成的,但是缓存还是需要遵守http规范的。
okhttp提供了 CacheStrategy 和 CacheControl两个类,这两个类会根据http的缓存规则,帮助我们完成实现缓存的一些工作。
CacheStrategy类顾名思义,就是缓存策略。他主要的工作就是根据各种条件判断 是向服务器发送请求 还是 直接从缓存中获取响应,或是会 对 请求报文进行一些设置。
CacheControl 类主要解析请求或响应报文中与缓存相关的字段。
有了这两个类的帮助,我们只需要完成 缓存的具体实现,而不需要额外自定义缓存选取的处理逻辑。
下面是InternalCache的接口源码:
可以看到我们自定义缓存时,要实现的方法只有常规的增删改查以及跟踪 缓存相关的请求和响应的两个方法。
public interface InternalCache {
Response get(Request request) throws IOException;
CacheRequest put(Response response) throws IOException;
/**
* Remove any cache entries for the supplied {@code request}. This is invoked when the client
* invalidates the cache, such as when making POST requests.
*/
void remove(Request request) throws IOException;
/**
* Handles a conditional request hit by updating the stored cache response with the headers from
* {@code network}. The cached response body is not updated. If the stored response has changed
* since {@code cached} was returned, this does nothing.
*/
void update(Response cached, Response network);
/** Track an conditional GET that was satisfied by this cache. */
void trackConditionalCacheHit();
/** Track an HTTP response being satisfied with {@code cacheStrategy}. */
void trackResponse(CacheStrategy cacheStrategy);
}
2.CacheInterceptor的工作流程
先来看CacheInterceptor的具体工作逻辑,下面是他的定义的变量以及Interceptor方法的源码:
final InternalCache cache;//自定义的缓存
public CacheInterceptor(InternalCache cache) {//通过构造方法传入
this.cache = cache;
}
//下面是intercept方法
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())//如果实现了内部缓存,那么就从内部缓存中取
: null;//如果没有,那么就为null
long now = System.currentTimeMillis();//获取目前的时间,后面要用于计算
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//根据返回的networkRequest和cacheResponse
//进行处理。总的来说,如果值为null那就不操作
0
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.
// 缓存响应无法获取(可能过期了,可能根本就没有缓存),但请求又要求了only-if-cached,只能返回504
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) {
//网络响应返回null(失败了),缓存响应不为null时,清空缓存
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
//如果我们有本地缓存响应,那么还会获取本地缓存响应的某些属性,然后进行设置
//ps:Response 类对象中会持有 网络响应(NetWorkResponse) 和缓存响应(CaCheResponse)
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {//响应码是304,需要用缓存响应
Response response = cacheResponse.newBuilder()//合并首部行,设置Response
.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()).
//机翻: 在合并标头之后但在剥离 Content-Encoding 标头之前更新缓存(由 // initContentStream() 执行)。
//更新本地缓存。之后响应会被传回BridgeInterceptor,那里会改变首部行
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;//返回给上一级拦截器(BridgeInterceptor)
} else {
//不是304的情况
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()//和上面的差不多,设置response
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {//回忆一下,cache是自定义的本地缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {//判断服务器返回的响应,能否在本地缓存
// Offer this request to the cache.
//把响应交给自定义的缓存,最后返回
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//HttpMethod.invalidatesCache方法判断该响应可以缓存
//结果却没有缓存,那么肯定是自定义的缓存有问题
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
Intercetor的大体流程如下:
分别将从责任链中取到的request和从缓存中去到的response分别赋给Networkrequest和Cacheresponse。
之后将Networkrequest和Cacheresponse。传入CacheStartay的Factory中。在build()方法里,会读取解析对应的请求报文和响应报文。并根据Http缓存的相关规则,对 Networkrequest和Cacheresponse进行设置。完成设置后,在Interceptor方法中决定是否从缓存中读取。
3.CacheStartay的get()方法
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
在Interceptpr方法中有上面这一段代码,通过调用CacheStrategy的Factory和get方法,我们获取
到networkRequest和cacheResponse并以此为根据,进行请求和获取缓存工作。
下面我们就来看一看这个两个方法的具体实现,了解okhttp具体的缓存存取策略。
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);
}
}
}
}
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.
//如果请求是https,但缓存响应缺少TLS握手,那么缓存也拜拜
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)) {//检查缓存响应,是否ok
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
//CacheControl 可以理解为也是一个缓存判断相关的类,判断的是http缓存
//而CacheStrategy负责与本地缓存打交道
if (requestCaching.noCache() || hasConditions(request)) {
//请求不需要缓存或者请求是有条件get请求(不拿本地缓存,要问服务器)的情况
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
//和上面不同,这个是缓存响应调用
long ageMillis = cacheResponseAge();//缓存响应的实际age
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());
}//只接收minFreshMills时间内未过期的缓存(从现在开始计算)
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}//过期后的时间不超过 maxStaleMillis 的话,缓存还能用
//下面就是判断缓存是否过期的相关计算
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.
//寻找协商缓存的相关字段并设置到请求首部行中,并移除requestBody(有条件Get请求的规定)
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);
}
4. 总结
再详细查看CacheInterceptor的源码之后,你会发现CacheInterceptor的工作流程其实并不复杂,总的来说,他只是根据http中关于缓存的相关规则 来做对应的处理。
下面是总结的几个场景,如果对http缓存的知识有一定的了解的话,这部分应该很好理解。
请求场景 | 处理结果 |
---|---|
没有自定义缓存(cache 为 null),且请求没有指明只用缓存 | 将请求传递给下一个拦截器以进行网络请求 |
缓存不可用或者根本没定义缓存,但请求指明only-if-cached(只用缓存) | 构建一个响应码为504的http响应并返回 |
有缓存,但请求是有条件GET请求 | 将请求传递给下一个拦截器以进行网络请求,并根据服务器返回的响应码决定是否选择缓存响应 |
有缓存,且缓存未过期 | 直接返回缓存响应,不进行网络请求 |
缓存过期,且无协商缓存相关首部字段 | 将请求传递给下一个拦截器以进行网络请求 |
缓存过期,但有协商缓存相关首部字段 | 对请求进行设置后,递给下一个拦截器以进行网络请求,并根据服务器返回的响应码决定是否选择缓存响应 |
5.Http缓存规则简单介绍
Http缓存大体可以分为强缓存和协商缓存。
强缓存的含义简单来说其实就是 服务器在相应报文中添加max-age字段,以标明缓存过期时间。没超过过期时间的资源,客户端就可以直接获取。
而协商缓存的意识就是客户端在获取缓存的时候,还需要根据缓存中的相关字段向服务器发出询问。如果收到服务器发来的响应码为304的报文,才能够使用缓存。
这里需要注意:强缓存的优先级是高于协商缓存的。
下面总结了CacheInterceptor的实现中,出现的一些缓存相关首部字段。
响应报文标识 | 含义 |
---|---|
Last-Modified | 资源的最后修改时间 |
Expires | 缓存有效时间,超过这个时间,缓存无效(这个单位是时间戳) |
ETag | 资源标识码,ETag改变说明资源已改变 |
max-age | 缓存可用的最大时间(单位是时间长度)请求报文中也可以有 |
请求报文标识 | 含义 |
---|---|
If-Modified-Since | 之前缓存中的Last-Modified值 |
If-None-Match | 之前缓存中的资源标识码 |
min-fresh | 希望收到 该时间长度内没有过期的缓存(从发出请求开始计时) |
max-stale | 即使过期了 一定的时间的缓存,也可以用(单位也是时间长度) |
only-if-cache | 只要缓存中的资源。如果缓存过期,一般会返回504 |
由于水平有限,有关http缓存的这部分知识可能有错误。想要详细了解的话,可以查阅相关的博客或者文章。
上面也只是记录一些缓存相关的常用字段,具体可以查看RFC规范
sintall.com/RFC - 码云 - 开源中国 (gitee.com)