前言
前一篇文章介绍了BridgeInterceptor,本篇我们介绍CacheInterceptor,望文知意,CacheInterceptor 肯定与数据的缓存有关系。
一 HTTP 缓存
在进入源码之前,这里需要介绍一下HTTP 协议中关于缓存控制的部分。
1.1 Cache-Control
1.1.1在请求中使用Cache-Control 时,它可选的值有:
Cache-Control 是用于告诉浏览器或者是代理服务器关于使用缓存的一些方式,这个字段对于最终的服务器而言没有什么作用。
app内部使用网络框架访问网跟浏览器没有什么关系,此时的浏览器或者代理服务器就是OKHttp 本身,OKHttp本身会读取用户设置的Cache-Control 字段,用户决定是否立即使用缓存还是先去服务器验证当前缓存时候有效再决定是否使用缓存。具体的我放到下面介绍。
no-cache 表示不使用缓存。
no-staore 表示此次请求的数据不应该被缓存。
no-transform 主要是用户代理服务器,例如代理服务器可能会将原服务器gzip压缩的数据解压之后再缓存并转发给用户,通过这个字段可以告诉代理服务器自己需要没有解压的数据。
only-if-cached 表示使用缓存,但是是由条件的使用缓存,也即是缓存不可以过期,若是想要使用过期缓存还需要添加上max-stable 字段。
1.1.2在响应中使用Cache-Control 时,它可选的值有
public
如果response被标记为“public”,那么,即使有与其相关的HTTP认证信息或者返回的response是不可缓存的status code,它依然可以被缓存。大多数情况下,public并不是必需的,因为其他具体指示缓存的信息,如max-age会表明当前的response在任何情况下都是要缓存的。
private
相比之下,浏览器可以缓存private的response。但是,这些响应通常只用于单个用户,因此不允许其他中间缓存对齐进行缓存。例如:一个用户的浏览器可以带有用户私有信息的HTML页面,但是CDN无法缓存页面。
no-cache 并不是不能使用缓存,而是在使用缓存之前需要先跟服务器验证缓存时候是否还有效。
no-store 这个才是真正的不能缓存数据。
1.1.3 标记
当服务器返回数据时候会返回Etag -或者Last-Modified 这个字段,Etag 是一个随机的字符串,Last-Modified 是一个时间戳,用于表明返回数据的版本。当用户需要使用缓存的时候可以先通过这个版本号查询缓存的数据时候有效,如果有效那么直接使用缓存。
Last-Modified 精确到秒,所有对于那么在一秒之内就会别刷新的数据是无效的,此时可以使用ETag,ETag 就是一个随机的字符串。
用户在请求的时候通过If-None-Match或者If-Match 将Etag 传递给服务器
通过If-Modified-Since或者If-Unmodified-Since将last-modified/ 传给服务器。
二、CacheInterceptor
2.1
@Override public Response intercept(Chain chain) throws IOException {
//获取缓存的Response
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.
//例如用户请求数据时设置了only-if-cached 但是第一次获取数据是云端
//返回的Cache-Control 为no-store
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();
}
// 到了这里表示networkRequest 或者 cacheResponse总有一个不是空的,也就是说或者访
//问网络,或者使用缓存
// If we don't need the network, we're done
//.networkRequest 是空,表示需要使用缓存数据
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) {
//云端返回304,也就是缓存数据有效,可以直接使用
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());
}
}
//到了这里表示cacheResponse 是 null,也就是之前没有缓存过数据,此时访问网络
//是从云端获取到最新的数据
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
//isCacheable 判断数据可否被缓存
//例如用户请求的Cache-Control为no-store 或者返回的Cache-Control为no-store
//此时数据就不会被缓存
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;
}
如上,基本改注释的地方都注释了,下面我们看一下
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
这一部分主要是判断需不需要使用缓存,如果使用缓存那么在使用缓存之前需不需要向服务器验证当前缓存的有效性。
public CacheStrategy get() {
//这是核心
CacheStrategy candidate = getCandidate();
//例如用户请求数据时设置了only-if-cached 但是第一次获取数据是云端
//返回的Cache-Control 为no-store
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() {
// No cached 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);
}
//判断是否可以使用缓存,例如cacheResponse或者request 的CacheControl
//为no-staore.
//一个小思考:
//假如第一次请求网络的时候request 没有设置CacheControl, 云端返回的数据的
//CacheControl 也不是no-staore,也就是说返回的数据被缓存了。
//第二次请求数据的时候request 设置CacheControl为no-store,此时也不会使用缓存
//是直接请求网络。因此请求端设置no-store 就表明一定要从新获取数据,
//类似no-cache
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();
//返回true 表明数据是永不变换的疏忽,此时可以直接使用缓存,
//具体的大家可以查询immutable
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
//age 的计算,简单理解为缓存数据产生到现在过去多久了。
//内部的计算考虑的东西比较多,不想研究的可以忽略
long ageMillis = cacheResponseAge();
//max-age的计算,计算数据在产生之后最多可以缓存的时间
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//比较请求的max-age 与 缓存的响应中的max-age ,取较小的那个
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
//同上,只是这里计算的是min-fresh
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
//用户说过期的资源我也要也就是用户设置了max-stale
//服务器说,缓存资源使用之前让我确认一下缓存还可以用吗,也就是返回CacheControl
// 是must-revalidate。
//一句话服务器返回 must-revalidate ,此时不论资源有没有过期,使用缓存之前,
//必须经过服务器的批准
//
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//条件为真表示允许使用缓存而且缓存没有超过指定的时间。
//假设一个缓存在中午12点过期,用户在使用缓存的时候希望缓存至少还有200s才过期
//minFreshMillis 表示的就是具体过期至少剩余的时间。
//同理 ,用户使用缓存的时候,假设缓存过期了,但是最大过期时间不能超过200s
//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.
//走到这里有一下几种情况
// 1 允许使用缓存,但是缓存过期 ,需要向服务器验证缓存有效性
// 2 允许使用缓存,缓存也没有过期,但是服务器返回CacheControl 为no-cache
String conditionName;
String conditionValue;
if (etag != null) {
//有etag优先使用etag
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);
}
2.1 时间的计算
上面的cacheResponseAge 方法主要是计算缓存的数据已经缓存了多久
private long cacheResponseAge() {
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;
}
服务端返回给客户端的数据中有一个头部信息Age,用户表示这个数据已经产生的时间,例如我们想代理服务器请求数据,可能这个数据在代理服务器已经存在一段时间了,Age 就是表示这个数据从源服务器产生已经过去的时间。
servedDate 表示的是服务端返回的头部信息Date,但是由于时区的差异,很多时候无法使用,我们这类认为是0
receivedResponseMillis 记录的是接收到这个缓存的时间点,
sentRequestMillis 记录的是发送请求的时间点。
nowMillis 表示当前时间,也就是第二次请求这个资源的时间点。
因此receivedAge + responseDuration + residentDuration 最终的结果就是缓存被缓存的时间,这个时间不单单包含客户端缓存的时间也计算了在代理服务器上缓存的时间。
三、CacheInterceptor
前面我们介绍了HTTP 缓存的相关信息以及OKHTTP 对相关协议的实现,但是在android平台是怎么缓存数据还没有介绍到,这一部分就来深入了解OKHTTP 对于数据的缓存实现。
当服务器返回数据之后,如果允许缓存此时会调用到 cache.put(response) 缓存数据。
cache的类型是InternalCache,这是一个接口,实际类型是顶一个Cache 类的构造函数里面的一个匿名内部类。
Cache(File directory, long maxSize, FileSystem fileSystem) {
//匿名内部类
this.internalCache = new InternalCache() {
public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
public CacheRequest put(Response response) throws IOException {
//实际调用的是是外部类Cache的put 方法缓存数据。
return Cache.this.put(response);
}
public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
this.cache = DiskLruCache.create(fileSystem, directory, 201105, 2, maxSize);
}
可以看到internalCache 的put 方法实际调用的是Cache 的put 方法
@Nullable
CacheRequest put(Response response) {
String requestMethod = response.request().method();
//代码有删减
Cache.Entry entry = new Cache.Entry(response);
Editor editor = null;
try {
editor = this.cache.edit(key(response.request().url()));
if (editor == null) {
return null;
} else {
entry.writeTo(editor);
return new Cache.CacheRequestImpl(editor);
}
} catch (IOException var7) {
this.abortQuietly(editor);
return null;
}
}
这里通过 cache 对象最终将数据保存在本地,cache 的实际类型是DiskLruCache
DiskLruCache并不是OKHttp 编写的,而是另外一个开源框架,关于这个框架大家可以自行查询使用方法,关于DiskLruCache 的源码解析介绍放到下一篇。