有了上一篇HTTP缓存机制的铺垫,现在我们来详细分析下CacheInterceptor
的实现原理,实际上在分析CacheInterceptor的工作原理前还应该熟悉一个专门做磁盘缓存的工具类DiskLruCache,它的原理很简单,使用方式和SharedPreferences类似,在这里不赘述了,有兴趣的童鞋可以参看下这篇博客Android DiskLruCache完全解析,硬盘缓存的最佳方案
前言
之前在逐个分析拦截器的时候,都是直接从intercept()
方法开始,在CacheInterceptor分析之前,先要了解两个概念,Cahce和Entry,那具体都是什么呢? 既然是操作缓存,那么就要有操作缓存的工具类和缓存实体。所以Cache
就是操作缓存的工具类,OkHttp是在DiskLruCache基础上进行了封装,实际上的缓存的读取还是使用DiskLruCache
,OkHttp添加了一些自己的属性判断而已,至于Entry,顾名思义就是缓存实体类,它是Cache
的一个静态内部类,它有很多属性,后边会详细说道。
intercept()方法
重点来了,一大波代码来袭,不过没什么关系,我做了大量注释,后面还有逐步的分析
@Override public Response intercept(Chain chain) throws IOException {
/**首先获取缓存数据,如果有缓存的话,暂且叫cacheCandidate为临时缓存备份,是一个临时的response,
* 后面要判断临时缓存备份是否可用*/
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
/**缓存策略,根据临时缓存备份和实际请求经过一些的条件判断,最终得到确定一个网络请求networkRequest和一个缓存cacheResponse,
* 二者都可能为null,
*/
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
/**networkRequest和cacheResponse在CacheStrategy中有定义**/
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
/**trackResponse方法的作用就是记录下网络请求的次数和缓存命中的次数*/
if (cache != null) {
cache.trackResponse(strategy);
}
/**如果缓存备份不为Null,并且经过缓存策略计算得到的真正的response为Null,
* 说明该缓存未命中,需要重新请求网络,所以临时缓存备份留着也没什么用了,就
* 可以关闭资源
*/
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.
/**无网但是有缓存的情况下 直接返回缓存数据*/
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
/**程序走到这一步时,说明networkRequest肯定不会是null,也就是说肯定是有网的状态
*那么有网的状态如何获取网络response?,还是调用下一组拦截器链来获得。
*/
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.
/**如果有缓存并且服务器返回的响应码是304,构建一个新的response,将缓存的内容
*融合到response里返回,并且更新缓存状态,如果不是304的响应码,不走缓存,缓存response就没
* 什么用了,关闭资源。
*/
if (cacheResponse != null) {
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());
}
}
/**走到这一步说明以上都不符合,只能使用网络响应*/
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
/**以下是根据HTTP缓存规则进行判断能否缓存,符合条件的话写入缓存*/
if (cache != null) {
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;
}
代码很长,一步一步分析:
读取缓存
/**首先获取缓存数据,如果有缓存的话,暂且叫cacheCandidate为临时缓存备份,是一个临时的response,
* 后面要判断临时缓存备份是否可用*/
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
上来第一步读取缓存,这里cache是缓存工具类Cache的对象,之前提到过Cache是对DiskLruCache的封装,我们看下它的put
和get
方法
put方法
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
/**判断该网络请求是否可以进行缓存*/
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
/**http的缓存只是针对于GET方法的,非GET直接返回*/
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
/**实例化一个缓存实体,通过DiskLruCache将该实体写入缓存*/
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
/**写入缓存的key是通过url的MD5加密再转换成16进制*/
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
/**这里只是写入了response的头部内容*/
entry.writeTo(editor);
/**真正服务器响应数据通过CacheRequestImpl写入缓存的*/
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
代码中注释很详细,我就不赘述了,和SharedPreferences
使用类似,只不过多了些条件判断
get方法
@Nullable Response get(Request request) {
/**通过url生成key(MD5、HEX)*/
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
/**通过key从内存中读取包装实体类Entry,内存中使用LinkedHashMap,
* 在通过实体获取到一个Snapshot,这些事内部实现,可以跟进查看
*/
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
/**通过快照snapshot得到一个Response实例*/
Response response = entry.response(snapshot);
/**匹配是否是符合要求的,是返回响应,否关闭*/
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
get方法的整体流程不再赘述,看注释就可以了,主要介绍下包装实体类Entry,无论是put
还是get
方法都使用了Entry对象,看下Entry是怎么写入缓存entry.writeTo(editor)和组装response的entry.response(snapshot)。
Entry
首先看下Entry的成员变量
private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";
/** Synthetic response header: the local time when the response was received. */
private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";
private final String url;
private final Headers varyHeaders;
private final String requestMethod;
private final Protocol protocol;
private final int code;
private final String message;
private final Headers responseHeaders;
private final @Nullable Handshake handshake;
private final long sentRequestMillis;
private final long receivedResponseMillis;
很明显都是一个网络请求的基本信息内容,没什么好解释的
接下来看下Entry是怎么写入缓存的
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();
}
以上很清晰看到写入缓存的操作都是通过Okio这个库中的输入流写入文件里的,没什么特殊的地方。
接着是读取数据转换成response
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
同样也是很简单的构建,这里不同的是真正响应体body
是通过CacheResponseBody进行读取的,我们跟进下CacheResponseBody
,看下具体实现
CacheResponseBody(final DiskLruCache.Snapshot snapshot,
String contentType, String contentLength) {
this.snapshot = snapshot;
this.contentType = contentType;
this.contentLength = contentLength;
Source source = snapshot.getSource(ENTRY_BODY);
bodySource = Okio.buffer(new ForwardingSource(source) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
});
}
发现还是通过Okio的读写流进行赋值。以上就是Cache缓存工具的内容,原理很简单,完全可以把它当做一个Map
或者SharedPreferences
想象,接下来我们继续重点分析拦截器的工作流程,继续分析** intercept**方法。
缓存策略配置
/**缓存策略,根据临时缓存备份和实际请求经过一些的条件判断,最终得到确定一个网络请求networkRequest和一个缓存cacheResponse,
* 二者都可能为null,
*/
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
/**networkRequest和cacheResponse在CacheStrategy中有定义**/
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
如果上一步获取到了缓存响应后,则配置缓存策略CacheStrategy,主要是配置CacheStrategy
的networkRequest和cacheResponse,我们具体看下CacheStrategy的源码:
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);
}
}
}
}
首先内部工厂构造方法中,我们可以到好多字段的判断,这些字段都是用来判断HTTP缓存的标识,具体HTTP的缓存机制怎么实现的,那就请参考上一篇博客浅析Http中的缓存机制,这个方法的主要就是用来解析这些响应标识的。接下来就是get()方法,获取一个CacheStrategy
实例。
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;
}
重要逻辑判断在getCandidate(),我们继续跟进
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
/**如果没有缓存响应,就返回一个没有响应的策略,这里cacheResponse的赋值在
* Factory方法传入的临时缓存备份赋值的,实际上就是该请求的缓存响应
*/
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);
}
// 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)) {
return new CacheStrategy(request, null);
}
/**缓存控制,不能缓存的返回一个没有响应的策略,具体判断用到的字段逻辑,参考HTTP的缓存机制*/
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
/**从这开始下面全都是通过响应头进行判断如何返回策略,具体的逻辑判断条件还是参考HTTP的缓存机制
*http://www.sherlockaza.com/2017/03/20/2017-03-20-http-cache/
*/
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 (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);
}
注释很详细,不一一解释了,不过可以看到,如果一个缓存响应能不能被使用要经过很多层的筛选。
缓存监测
/**trackResponse方法的作用就是记录下网络请求的次数和缓存命中的次数*/
if (cache != null) {
cache.trackResponse(strategy);
}
/**如果缓存备份不为Null,并且经过缓存策略计算得到的真正的response为Null,
* 说明该缓存未命中,需要重新请求网络,所以临时缓存备份留着也没什么用了,就
* 可以关闭资源
*/
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
记录缓存命中次数和请求次数,不需要的资源释放,closeQuietly后边会多次用到,主要作用是关闭资源,有兴趣的童鞋跟进代码可以看到了,实际就是关闭了response
中数据流。
无网无缓存
/**禁网的情况下直接抛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();
}
禁网并且没有缓存数据,直接返回504
无网有缓存
// If we don't need the network, we're done.
/**无网但是有缓存的情况下 直接返回缓存数据*/
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
无网有缓存状态直接返回缓存响应。
有网缓存无效
/**程序走到这一步时,说明networkRequest肯定不会是null,也就是说肯定是有网的状态
*那么有网的状态如何获取网络response?,还是调用下一组拦截器链来获得。
*/
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.
/**如果有缓存并且服务器返回的响应码是304,构建一个新的response,将缓存的内容
*融合到response里返回,并且更新缓存状态,如果不是304的响应码,不走缓存,缓存response就没
* 什么用了,关闭资源。
*/
if (cacheResponse != null) {
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());
}
}
当存在缓存响应时,如果请求响应码是304,说明该缓存有效未做更改,继续使用缓存,则返回该响应,并将缓存命中计数器+1
,更新下当前缓存状态,如果响应码不是304,那么说不能使用缓存,就把缓存资源关闭。
使用网络响应
/**走到这一步说明以上都不符合,只能使用网络响应*/
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
以上条件都不符合时,使用网络响应。
写入缓存
/**以下是根据HTTP缓存规则进行判断能否缓存,符合条件的话写入缓存*/
if (cache != null) {
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.
}
}
}
经过一些条件判断,将请求下来的网络响应写入缓存,下次读取使用。
总结
CacheInterceptor
的分析比较之前的分析内容比较多,只要了解HTTP的缓存机制
,理解起来也不是很复杂,在Android实际开发中,用GET的请求方式时候非常少,所以用到缓存策略的机会也比较少,所以真正的客户端数据缓存还得靠自己写,但是OkHttp的缓存思想我们还是可以借鉴的。最后还是一个概括的流程图结束:
网友评论