目录
-
主流程分析
Okhttp的缓存策略主要是通过拦截器 CacheInterceptor 来实现,关于拦截器的拦截流程请参考关于OkHttp的拦截器执行流程源码分析。
-
代码分析
关于CacheInterceptor我们从代码中一步步来分析,先从 intercept(Chain chain) 方法开始分析。
public Response intercept(Chain chain) throws IOException {
//1.获取候选缓存数据
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
//2.获取缓存策略
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.
}
//3.根据缓存策略,没有有效的request,并且没有有效缓存,响应504
// If we're forbidden from using the network and the cache is insufficient, fail.
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();
}
//4.根据缓存策略,没有有效的request,不进行网络请求,直接返回候选缓存
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//5.根据缓存策略,具有有效的request,执行网络请求,获取在线数据
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());
}
}
//6.如果候选缓存不为空,并且在线数据httpcode为304,则使用候选缓存,并更新缓存数据
// If we have a cache response too, then we're doing a conditional get.
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());
}
}
//7.根据在线数据,构建响应实例
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//8.缓存数据
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;
}
-
代码总结
在以上代码中我们可以看到缓存拦截器对缓存的处理,共有以下几个步骤:
-
获取候选缓存数据;
-
根据候选缓存数据,获取缓存策略;
-
根据缓存策略,没有有效的request,并且没有有效缓存,响应504;
-
根据缓存策略,没有有效的request,不进行网络请求,直接返回候选缓存;
-
根据缓存策略,具有有效的request,执行网络请求,获取在线数据;
-
如果候选缓存不为空,并且在线数据httpcode为304,则使用候选缓存,并更新缓存数据;
-
根据在线数据,构建响应实例;
-
缓存数据;
-
具体步骤分析
通过以上的步骤,缓存拦截器完成了缓存的获取,更新,存储等,下面来具体分析一下每个步骤。
-
获取缓存候选数据
在这个步骤中一共有两个前提条件 1.设置了cache。2.有历史缓存数据。满足这两个条件则可以通过request取到缓存数据。
-
设置缓存
cache是在CacheInterceptor构造中方法中传入的,而 CacheInterceptor 的构造方法传入就是 client.internalCache()。client.internalCache()就是通过 builder.cache(new Cache()) 设置,所以在这里我们也可以看到为什么要通过builder 设置cache。
-
历史缓存数据
是在其他步骤中获取到的数据缓存到本地的历史数据。
-
通过request获取缓存数据
Response get(Request request) {
//1.通过 url 获取 MD5 key
String key = key(request.url());
//2.获取快照,快照不可用则return null
Snapshot snapshot;
try {
snapshot = this.cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException var7) {
return null;
}
//3.通过快照创建entry
Cache.Entry entry;
try {
entry = new Cache.Entry(snapshot.getSource(0));
} catch (IOException var6) {
Util.closeQuietly(snapshot);
return null;
}
//4.获取候选的response对象
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
} else {
return response;
}
}
cache是通过request作为key获取的,在这段代码的第2步,是通过url作为key向cache中去取snapshot快照,在后面我们也可以看到通过url作为key向cache中put snapshot数据。
-
根据选数据获取缓存策略
从下面的代码中我们可以看到通过当前的时间戳、请求信息、缓存候选数据获得了一个缓存策略。缓存策略的创建规则如下:
- 缓存无效,返回networkRequest请求实体
- 缓存有效,但是请求是https并且http握手信息丢失,返回networkRequest请求实体
- 缓存数据不可以被复用,返回networkRequest请求实体
- 请求的缓存管理,强制要求不使用缓存,或者请求包头中包含“If-Modified-Since”或者“If-None-Match”字段,返回networkRequest请求实体
- 缓存有效,并且在有效的时间范围内,则返回cacheResponse缓存
- 如果缓存中“ETag”、“If-Modified”、“Date”等字段不为空则创建带参请求,返回conditionalRequest, cacheResponse。否则返回networkRequest请求实体
代码主要在CacheStrategy getCandidate 方法
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
//1.缓存无效,返回networkRequest请求实体
// No cached response.
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//2.缓存有效,但是请求是https并且http握手信息丢失,返回networkRequest请求实体
// Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//3.缓存数据不可以被复用,返回networkRequest请求实体
// 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);
}
//4.请求的缓存管理,强制要求不使用缓存,或者请求包头中包含“If-Modified-Since”或者“If-None-Match”字段,返回networkRequest请求实体
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//5.缓存有效,并且在有效的时间范围内,则返回cacheResponse缓存
CacheControl responseCaching = cacheResponse.cacheControl();
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());
}
//6.如果缓存中“ETag”、“If-Modified”、“Date”等字段不为空则创建带参请求,返回conditionalRequest, cacheResponse。
//否则返回networkRequest请求实体
// 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);
}
-
缓存数据;
判断是否为有效的response数据,包括是否存在body和是否可缓存,主要根据http协议约定。放入cache 队列
CacheRequest put(Response response) {
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
this.remove(response.request());
} catch (IOException var6) {
}
return null;
} else if (!requestMethod.equals("GET")) {
return null;
} else if (HttpHeaders.hasVaryAll(response)) {
return null;
} else {
// 有效的缓存数据,构建包含editor的CacheRequestImpl实例
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;
}
}
}
在上面的代码中看到,在这里显示判断了缓存的可用性,然后构建了包含editor的CacheRequestImpl实例,在这里缓存并没有真正的写入文件,缓存并不可用。
写入缓存 cacheWritingResponse
/** * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we * may never exhaust the source stream and therefore not complete the cached response. */ private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response) throws IOException { // Some apps return a null body; for compatibility we treat that like a null cache request. if (cacheRequest == null) return response; Sink cacheBodyUnbuffered = cacheRequest.body(); if (cacheBodyUnbuffered == null) return response; final BufferedSource source = response.body().source(); final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered); Source cacheWritingSource = new Source() { boolean cacheRequestClosed; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead; try { bytesRead = source.read(sink, byteCount); } catch (IOException e) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheRequest.abort(); // Failed to write a complete cache response. } throw e; } if (bytesRead == -1) { if (!cacheRequestClosed) { cacheRequestClosed = true; cacheBody.close(); // The cache response is complete! } return -1; } sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead); cacheBody.emitCompleteSegments(); return bytesRead; } @Override public Timeout timeout() { return source.timeout(); } @Override public void close() throws IOException { if (!cacheRequestClosed && !discard(this, ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) { cacheRequestClosed = true; cacheRequest.abort(); } source.close(); } }; String contentType = response.header("Content-Type"); long contentLength = response.body().contentLength(); return response.newBuilder() .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource))) .build(); }
在上一段我们提交,缓存put之后并没有真正的写入文件,缓存并不可用,在这一步骤中可以看到原因。只有在Callback.onResponse中读取了字节流信息,才会关闭流,并完成缓存。这里可以看到调用editor.commit
public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
}
而只有调用了editor.commit,entry.readable设置为完,缓存才算是完成,后续才可以使用该缓存。我们可以看到在get的时候会判断readable这个标志
public synchronized Snapshot get(String key) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
// 判断标志位,缓存是否可用
if (entry == null || !entry.readable) return null;
Snapshot snapshot = entry.snapshot();
if (snapshot == null) return null;
redundantOpCount++;
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
return snapshot;
}
到这里缓存已经可用了。
-
更新缓存
看过上文分支的分析,更新缓存这里就比较简单了,主要是cache的update方法中,下面我们来看代码
void update(Response cached, Response network) {
Entry entry = new Entry(network);
DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
DiskLruCache.Editor editor = null;
try {
editor = snapshot.edit(); // Returns null if snapshot is not current.
if (editor != null) {
entry.writeTo(editor);
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
从上面代码中可以看到通过cached获取到editor,写入并commit提交,完成了缓存的更新。