OkHttp源码解析(四)缓存机制
CacheInterceptor-> intercept
@Override public Response intercept(Chain chain) throws IOException {
// 先从缓存中获取,request是key
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
// 获取当前时间
long now = System.currentTimeMillis();
// 传入request、response拿到一个缓存策略对象
// 其实就是把request、response封装起来, 判断是否符合要求
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 如果cache存在则增加计数器
if (cache != null) {
cache.trackResponse(strategy);
}
// 从cache中得到了这个缓存,但是从策略缓存中没有得到,说明这个缓存是无效
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.
// 如果这个请求不使用网络,而且缓存拿不到,那就直接返回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.
// 如果仅仅需要缓存,就直接返回缓存的response
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 接下来就是走网络去拿response
Response networkResponse = null;
try {
// 从下层的拦截器拿到response
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) {
// 如果响应码是304,那就说明从上次请求之后,页面内容没有发生改变
if (networkResponse.code() == 304) {
Response response = cacheResponse.newBuilder()
// 混合缓存response的头部与网络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()).
cache.trackConditionalCacheHit();
// 更新缓存
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
// 在新的response中添加信息
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.
// 如果这个response是可以被缓存的
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;
}
方法稍微有点长,总结一下方法的流程:
- 从Cache中以request为key获取response
- 根据request、当前时间、response构建一个缓存策略,主要作用就是判断是否符合缓存要求
- 然后判断这个Cache中获取的缓存是否有效
- 如果缓存策略不采用网络,判断依据就是策略中networkRequest是否为null了,如果不采用网络,而且缓存又拿不到就直接504,如果不采用网络,缓存中也有,那就直接返回缓存就好了
- 走完缓存的逻辑之后就是通过拦截器机制,从下层的拦截器中通过网络拿response
- 然后对于304这个响应码作出特殊处理,把本地和网络响应头合并之后返回,当然还要更新缓存
- 而对于其他情况,肯定就是判断经历网络请求后这个响应是否能满足缓存条件,能满足就缓存下来
总结下来就是缓存策略是根据CacheStrategy中的networkRequest和cacheResponse来决定,所以有四种情况
- networkRequest == null, cacheResponse == null:不采用网络,也拿不到缓存,返回一个504的response
- networkRequest != null, cacheResponse == null:采用网络,不走缓存
- networkRequest == null, cacheResponse != null:不采用网络,走缓存获取
- networkRequest != null, cacheResponse != null:取网络,然后把网络拿到的response更新到缓存中
InternalCache
public interface InternalCache {
@Nullable Response get(Request request) throws IOException;
@Nullable 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);
}
在CacheInterceptor中的InternalCache是缓存机制中设计的一个接口,主要还是定义了一些增删改查的接口,提供给实现一个规范
这个Cache可以在OkHttpClient构建的时候配置进来,而Ok里面也默认有一个实现
Cache# internalCache
final InternalCache internalCache = new InternalCache() {
@Override public @Nullable Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public @Nullable CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
Cache-> put
@Nullable CacheRequest put(Response response) {
// 获取response对应的request
String requestMethod = response.request().method();
// 对Http请求方式判断
// 如果是post、patch、put、delete、move其中一个
// 就清除缓存并退出
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// 不要缓存非GET响应, 从技术上讲,我们可以缓存
// HEAD请求和一些POST请求,但是这样做很复杂,因此收益低。
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
// 根据response创建一个entry
// 然后使用LruCache执行缓存写入
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
Ok默认的缓存逻辑就是根据请求方式判断相应是否添加到缓存中
然后其实就是以request.url生成key,然后借用DisLruCache去做实际的缓存操作
而其实Ok默认会缓存也只有get这种请求方式了,Ok的作者也提到,可以head做缓存,但是太复杂了,收益太低,所以没有在Ok中去实现
Cache-> key
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
key的计算规则其实就是md5加密,然后以utf8编码生成String
Cache# Entry
Entry(Response response) {
this.url = response.request().url().toString();
this.varyHeaders = HttpHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
this.sentRequestMillis = response.sentRequestAtMillis();
this.receivedResponseMillis = response.receivedResponseAtMillis();
}
可见其实Entry会获取response中的信息,然后保存起来,而这其实就是把response中的缓存信息抽象出来形成一个数据结构了
Entry-> writeTo
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
sink.writeUtf8(requestMethod)
.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(new StatusLine(protocol, code, message).toString())
.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size() + 2)
.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n');
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n');
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
}
sink.close();
}
然后其实就是把Entry中保存的数据,根据文件规则,通过Okio写入到文件中
然后其实整个Cache对response的处理都是类似的了,把response转换成entry,然后用Okio写到文件中
读取的时候也一样也采用Okio,然后把entry转换为response即可
而DisLruCache是负责对写入的文件的管理,避免缓存过多占用过多本地的空间,再达到一定大小时就进行清除
小结
到这里其实Ok中缓存的存取机制都已经走了一遍了,其实本质就是基于DisLruCache对本地文件添加lru策略,而写入与读写便依赖Okio,然后Entry便是response与写入内容的一道枢纽
CacheStrategy.Factory
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);
}
}
}
}
把request和缓存中拿到response传给缓存的这个Factory,在这里主要做了一些成员的初始化工作
而针对缓存中response所对应的头部,会取出一些与缓存相关的字段:
- Date:请求的时间
- Expires:服务器返回的到期时间
- Last-Modified:资源最后修改时间
- ETag:资源的唯一标识符
- Age:代表服务器在多久前创建了响应,单位秒
CacheStrategy-> get
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;
}
这里只有针对不采取网络并且Cache-Control为only-if-cached,给予了网络和缓存都为null,也就是直接504了
CacheStrategy-> getCandidate
private CacheStrategy getCandidate() {
// No cached response.
// 如果没有缓存的response,则采取网络
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
// 如果是https而且丢失了一些握手相关的信息,则也不走缓存
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 如果这个response是不支持缓存的,则也不走缓存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
// 拿到请求中请求头中Cache-Control
CacheControl requestCaching = request.cacheControl();
// 如果头部不支持缓存,则也不走缓存
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 拿到响应中响应头的Cache-Control
CacheControl responseCaching = cacheResponse.cacheControl();
// 计算当前response的存活时间以及缓存应当被刷新的时间
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-None-Match、If-Modified-Since等头部字段进行处理
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.
}
// 若存在上述头部,则在原request中添加对应header,之后结合本地cacheResponse创建缓存策略
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);
}
主要针对机制情况给出缓存策略:
- 如果没有缓存的response,那策略就是直接走网络
- 如果是Https而且缺少握手的信息,那策略也是直接走网络
- 如果响应的响应码不支持缓存,策略也是走网络
- 如果响应的头部不支持缓存,策略就也还是走网络
- 所以策略能走缓存的情况就是响应码支持缓存,而且有相应的头部,而且还没有超过缓存时间
CacheStrategy-> isCacheable
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
case HTTP_NOT_AUTHORITATIVE:
// 204
case HTTP_NO_CONTENT:
// 300
case HTTP_MULT_CHOICE:
// 301
case HTTP_MOVED_PERM:
// 404
case HTTP_NOT_FOUND:
// 405
case HTTP_BAD_METHOD:
// 410
case HTTP_GONE:
// 414
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
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.
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();
}
主要的逻辑还是根据响应码区分情况处理,然后根据响应头的Cache-Control和request中的Cache-Control是否为no-store而决定是否支持缓存
而针对307也就是重定向这种情况下,除了Cache-Control之外还需要配置了Expires这个响应头才能支持缓存,所以这里会添加其他一些头部字段的判断
总结
先总结一下这个拦截器的职责分工还有基本机制
- CacheIntercepter:对缓存进行实际操作的对象,在拦截器的拦截逻辑中,会根据本地response的缓存情况,请求的数据向CacheStrategy请求一个策略,然后CacheIntercepter就根据策略,以及Cache提供的操作缓存的接口,做实际缓存以及网络请求的工作
- CacheStrategy:根据本地缓存,以及请求的情况,根据响应码,头部的字段给出缓存的策略,主要的策略有:
- networkRequest == null, cacheResponse == null:不采用网络,也拿不到缓存,返回一个504的response
- networkRequest != null, cacheResponse == null:采用网络,不走缓存
- networkRequest == null, cacheResponse != null:不采用网络,走缓存获取
- networkRequest != null, cacheResponse != null:取网络,然后把网络拿到的response更新到缓存中
- Cache:实际的缓存逻辑由DiskLruCache来完成,保证了占用空间不会太大,而写入以及读取的操作交由Okio来完成,然后对外提供接口进行crud的操作
再总结一下Http协议的知识,看Ok感觉就像从头开始实现一个Http协议一样,而CacheInterceptor这一层拦截器的主要功能就是支持Http协议的缓存机制:
Http协议中分为两种缓存方式:
- 强制缓存:强制缓存不需要和服务器产生交互
- 对比缓存:对比缓存不管是否生效,都需要与服务端发生交互
两类缓存规则可以同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则
对于强制缓存来说,header中会有两个字段来标明失效规则(Expires/Cache-Control),指的是当前资源的有效期
Expires/Cache-Control规则
Expires Expires的值为服务端返回的到期时间,在响应http请求时告诉客户端在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。Expires 的一个缺点就是,返回的到期时间是服务器端的时间,这样存在一个问题,比较的时间是客户端本地设置的时间,所以有可能会导致差错,所以在HTTP 1.1版开始,使用Cache-Control替代。
Cache-Control 用于定义所有的缓存机制都必须遵循的缓存指示,这些指示是一些特定的指令,包括public、private、no-cache(表示可以存储,但在重新验正其有效性之前不能用于响应客户端请求)、no-store、max-age、s-maxage以及must-revalidate等;Cache-Control中设定的时间会覆盖Expires中指定的时间;
对比缓存 对比缓存,顾名思义,需要进行比较判断是否可以使用缓存 浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据
在对比缓存生效时,状态码为304,并且报文大小和请求时间大大减少。 原因是,服务端在进行标识比较后,只返回header部分,通过状态码通知客户端使用缓存,不再需要将报文主体部分返回给客户端
对于对比缓存来说,缓存标识的传递是我们着重需要理解的,它在请求header和响应header间进行传递,一共分为两种标识传递
Last-Modified/If-Modified-Since规则
Last-Modified: 服务器在响应请求时,告诉浏览器资源的最后修改时间。
If-Modified-Since: 再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回的资源最后修改时间。 服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。 若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200; 若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache。
Etag/If-None-Match规则(优先级高于Last-Modified/If-Modified-Since)
Etag: 服务器资源的唯一标识符, 浏览器可以根据ETag值缓存数据, 节省带宽. 如果资源已经改变, etag可以帮助防止同步更新资源的相互覆盖. ETag 优先级比 Last-Modified 高.
If-None-Match: 再次请求服务器时,通过此字段通知服务器客户段缓存数据的唯一标识。 服务器收到请求后发现有头If-None-Match 则与被请求资源的唯一标识进行比对, 不同,说明资源又被改动过,则响应整片资源内容,返回状态码200; 相同,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用所保存的cache
不能缓存的请求
- 不能被缓存的请求HTTP 信息头中包含Cache-Control:no-cache,pragma:no-cache,或Cache-Control:max-age=0 等告诉浏览器不用缓存的请求
- 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
- 经过HTTPS安全加密的请求,且获取不到握手信息
- HTTP 响应头中不包含 Last-Modified/Etag,也不包含 Cache-Control/Expires 的请求无法被缓存
- 目前浏览器的实现是不会对POST请求的响应做缓存的(从语义上来说也不应该),并且规范中也规定了返回状态码不允许是304。不过这并不表示POST的响应不能被缓存,根据RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content中描述的,如果在POST请求对应的响应中包含Freshness相关信息的话,这次响应也是可以被缓存
参考资料:http协议缓存机制