基本流程
CacheInterceptor,主要的步骤已经在下面代码中注释了出来,最关键的就是第二步,单独分析一下这一步
@Override public Response intercept(Chain chain) throws IOException {
// 1、get cache
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 2、根据cache头获取缓存策略,ref:https://tools.ietf.org/html/rfc7234
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);
}
// 清理cache
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// 3、策略中的request和response都是空,是异常情况,构造504 Gateway Timeout错误
// 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、不需要进行请求,直接返回缓存
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 5、发起真正的请求
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、网络请求返回304 not-modified,直接返回并更新缓存
// 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 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);
// 9、返回真正的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();
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
// 疑问,这里为什么的把cache response也置为null返回
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;
}
/** Returns a strategy to use assuming the request can use the network. */
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.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 根据response code和no-store判断是否允许缓存
// 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);
}
// requst不需要使用cache,或者没有If-Modified-Since/If-None-Match无法使用缓存
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 下面这一段,计算缓存是否在有效期内,ref:https://tools.ietf.org/html/rfc7234
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;
CacheControl responseCaching = cacheResponse.cacheControl();
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());
}
// 有缓存同时需要再发起请求的,在请求头中添加If-Modified-Since/If-None-Match
// 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);
}
构造缓存策略,就是根据http协议头的规定来决定是否使用缓存,可以参考https://tools.ietf.org/html/rfc7234。这里还有一个疑问,在candidate.networkRequest != null && request.cacheControl().onlyIfCached()时候把cache response置空然后返回了,不理解为什么这么做。
缓存类的实现
缓存实现类主要有Cache和DiskLruCache。Cache相当于一个wrapper,主要的实现还是在DiskLruCache。先大概看一下缓存的实现机制再梳理一下流程
DiskLruCache实现方案
/*
* This cache uses a journal file named "journal". A typical journal file
* looks like this:
* libcore.io.DiskLruCache
* 1
* 100
* 2
*
* CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
* DIRTY 335c4c6028171cfddfbaae1a9c313c52
* CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
* REMOVE 335c4c6028171cfddfbaae1a9c313c52
* DIRTY 1ab96a171faeeee38496d8b330771a7a
* CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
* READ 335c4c6028171cfddfbaae1a9c313c52
* READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
*
* The first five lines of the journal form its header. They are the
* constant string "libcore.io.DiskLruCache", the disk cache's version,
* the application's version, the value count, and a blank line.
*
* Each of the subsequent lines in the file is a record of the state of a
* cache entry. Each line contains space-separated values: a state, a key,
* and optional state-specific values.
* o DIRTY lines track that an entry is actively being created or updated.
* Every successful DIRTY action should be followed by a CLEAN or REMOVE
* action. DIRTY lines without a matching CLEAN or REMOVE indicate that
* temporary files may need to be deleted.
* o CLEAN lines track a cache entry that has been successfully published
* and may be read. A publish line is followed by the lengths of each of
* its values.
* o READ lines track accesses for LRU.
* o REMOVE lines track entries that have been deleted.
*
* The journal file is appended to as cache operations occur. The journal may
* occasionally be compacted by dropping redundant lines. A temporary file named
* "journal.tmp" will be used during compaction; that file should be deleted if
* it exists when the cache is opened.
*/
这是DiskLruCache的注释。首先有一个记录文件journal,记录了每个缓存每次的==状态变化==,每个缓存的状态和大小;状态分为4中:DIRTY - 临时缓存,通常后续会变为CLEAN/READ状态,在初始化读取文件时,不会读取到内存中;CLEAN - 可用的缓存;READ - 缓存已经被读取过;REMOVE - 缓存需要删除。也就是说,对一个缓存会有多条记录,每条记录对应了一次状态的变更。
保存缓存DIRTY(这时候只生成了一条记录和一个记录response head的文件) -> 调用方通过read方法读取response,在返回response的同时,生成记录response body的文件 -> 下次请求读取了该缓存,记录状态改为READ;过程中有异常或者缓存的size超过了max size,状态改为REMOVE
缓存的存取流程
初始化
在put和get的过程中,如果还没有进行过初始化,就会进行初始化。主要的方法是readJournal和processJournal。
流程就是遍历journal文件的内容,最终的结果,如果是CLEAN,就会在LinkedHashMap里生成一个节点
private void readJournal() throws IOException {
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
try {
...
int lineCount = 0;
while (true) {
try {
// 读取每一行并处理
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
...
}
private void readJournalLine(String line) throws IOException {
...
// 如果是REMOVE的话,在链表中删除该节点
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
// 如果不是REMOVE,先把该节点加入链表,后续会进一步处理
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
private void processJournal() throws IOException {
fileSystem.delete(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
// 如果最终状态是CLEAN或者READ,对缓存大小进行累加
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
// 如果状态是DIRTY(在readJournalLine中赋值的),就在链表中删除该节点
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
fileSystem.delete(entry.cleanFiles[t]);
fileSystem.delete(entry.dirtyFiles[t]);
}
i.remove();
}
}
}
缓存读取 - get
缓存的读取比较简单,就是返回链接中对应的节点,然后在journal文件中增加READ状态。==注意,缓存里面存储的都是数据的source,而不是真正的数据,数据是在调用者需要读取的的时候才读到内存里的,下同==
缓存存储 - update/put
先看看基本流程
@Nullable
CacheRequest put(Response response) {
// 确定是否可以缓存,只有GET才能够缓存
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
// 1、生成editor
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
// 2、向文件写入header数据
entry.writeTo(editor);
// 3、构造cache request,很重要!
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
第一步生成editor比较简单,这一步主要做3件事:写入DIRTY记录;生成Entry;生成Editor,主要代码如下,比较清晰
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
...
// 向journal文件写入一条DIRTY记录
// Flush the journal before creating files to prevent file leaks.
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
...
// 实例化Entry存入链表,最后返回editor
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
第二步将response头写入文件,需要注意的是,header和body是要分别写入不同的文件的,而且这时候只写了header,是没有写body的,写body是在第三步
第三步,Cache.put方法的返回值是CacheRequest,通过如下代码构造的,主要的部分就是生成了body文件对应的sink
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
// 生成body对应的文件的sink
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
};
}
CacheRequest返回给CacheInterceptor之后,Interceptor会通过cacheWritingResponse方法构造response,这里会构造body对应的缓存文件的source,方法如下
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) {
...
}
...
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
...
};
return response.newBuilder()
.body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
.build();
}
之后,如果调用者通过response.body().soruce().readUtf8() - 或者其他read方法读取数据,就会调用到read方法,其中两行
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
就会将缓存写入文件。在全部数据读取完成后,会调到source.close(),进一步调用到sink.close(),这个方法在CacheRequestImpl的构造方法中,这个close会再调到editor.commit(),最后执行的方法是completeEdit(),completeEdit的处理比较清晰:1、DIRTY状态下写入的文件是tmp文件,最后写入成功后,重命名;2、成功的话,修改状态为CLEAN,失败的话修改状态为REMOVE。
到这里,缓存文件写入就完成了。
还有点疑问:在用户读取的时候才写入body可以理解为节省空间,那为什么要先写入header呢,如果是DIRTY状态,header应该也没用才对。设计这么多状态并且记录每次状态的变更有什么好处?
另外,cache是跟network response一起返回的,对于GET请求,在无网络的情况下应该可以很快返回,但是对于有网络但是请求处理比较慢的情况下,UI想先显示cache后显示再根据network response更新界面的需求无法用默认缓存实现,需要自己实现前置Interceptor实现。