总结
不缓存 POST、HEAD、PUT 请求- 缓存文件一共有两个,
缓存文件名 key.0 与 key.1,前者存储响应头,后者存储响应体。其中 key 是 md5(url) 后的值。只缓存响应,不缓存请求- 另外,如果请求是 https,key.0 还会存储 https 握手相关的一些信息
只有读取返回的数据时,响应才会被缓存,并不是说拿到后台返回结果后立即就缓存到本地- 响应头不需要等到数据返回数据,它会首先缓存到 tmp 文件中,在读取数据时 tmp 文件会被重命名成为正式缓存文件。这时才是真正的缓存
- 响应体只有在读取数据时才会被写到 tmp 文件中,然后重命名为正常文件
CacheControl
跟 http 缓存协议相关的主要是 cache-control 头,okhttp 提供了 CacheControl 用于处理 cache-control。大略了解一下里面的几个属性
FORCE_NETWORK
源码为
CacheControl FORCE_NETWORK = new Builder().noCache().build();
可以看到就是将 cache-control 设置为 no-cache,表示使用协商缓存。协商缓存肯定会调用服务器
onlyIfCached
仅使用在有效期内的缓存。如果缓存已过期,就会返回错就,状态码是 504
FORCE_CACHE
仅使用缓存,不论缓存是否过期。它的实现是先设置成仅使用缓存,然后将 stale 设置的时间非常大。stale 的值表示缓存过期多久后还可以使用,它的值设置很大时就可以使用已过期的缓存
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
类
主要涉及的类有
- CacheInterceptor:缓存拦截器,由该类触发缓存的所有操作,算是缓存的入口类
- CacheStrategy:缓存策略类。根据请求头、响应头(已有缓存的响应头)决定当前缓存是否可用。它就是对 http 协议中关于缓存部分的实现
- Cache:缓存存储类,内部使用 DiskLruCache 将缓存写入到硬盘文件中,也是外界传给 okhttp 内部的 Cache 对象
- InternalCache:CacheInterceptor 用于操作缓存的对象,它实际上是将所有操作都转给了 Cache 对象
- DiskLruCache:Cache 用之与硬盘中的文件交互
类初始化
在使用 OkHttp 的缓存时,首先需要的就是调用 OkHttpClient.Builder.cache() 方法,然后传入一个 okhttp3.Cache 对象,该类在构造时就会创建 DiskLruCache 对象
// okhttp3.Cache 构造函数
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
同时 Cache 有一个 internalCache 字段,它是 InternalCache 的子类,这个字段最终会传给 CacheInterceptor
// Cache.java
final InternalCache internalCache = new InternalCache() {
@Override public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public 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);
}
};
http 缓存协议的实现
CacheInterceptor
缓存的入口,同时也是处理缓存的整个逻辑。
@Override public Response intercept(Chain chain) throws IOException {
// 从本地读缓存。cache 是 InternalCache 实例
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 解析 http 缓存相关的请求头、响应头,决定是否返回 request、response
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 经过上面 CacheStrategy 的一通处理,已拿到了缓存。处理过程后面再说
// 即没有网络请求,也没有缓存(可能没有,也可能是无法使用),直接返回错误
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 (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.
// 状态码 304,就将本地的缓存改改,然后返回
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();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// 添加缓存,被缓存的前提条件是有 响应体以及可以缓存。像 HEAD 请求就没有响应体,所以也不会被缓存
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
// POST、PUT 请求时会返回 true。也就是 POST、PUT 请求不能缓存
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
CacheStrategy
intercept() 中先构造了内部类 Factory 的实例,然后调用 Factory 的 get 方法。
Factory 构造函数
逻辑简单,就是读取响应头中的 ETag, Expires, Last-Modified 等跟缓存相关的请求头,然后存储起来。这些字段都是跟缓存相关的,在 get 时会使用这些属性判断缓存。
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);
// 读取响应头中的 Date 字段
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
// 读取响应头中的 Expires 字段
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
// 读取响应头中的 Last-Modified 字段
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
// 读取响应头中的 ETag 字段
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
// 读取响应头中的 Age 字段
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
get
利用 Factory 读取的各个信息,按 http 规则决定是否缓存
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached())
// 单独处理 cache-control: only-if-cached。此字段表示客户端不进行网络请求
// 所以注释才说 forbidden from using the network
// We're forbidden from using the network and the cache is insufficient.
// 因为禁止使用网络,所以第一个参数为 null
return new CacheStrategy(null, null);
}
return candidate;
}
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
only-if-cached 表示只使用缓存,因此返回的 CacheStrategy 两个参数都应该是 null。这里有一个问题,如果有缓存这里怎么返回?其实在 getCandidate() 中,如果有缓存且可用时,networkReq 会为 null,也就是 if 判断不成立。
getCandidate
这个方法基本上就是对 http 中关于缓存协议的实现。看一下
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);
}
// isCacheable() 主要看的是请求头与响应头中 cache-control 中是否设置过 no-store(表示不缓存)
// 这是对不缓存 http 协议的实现
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
// noCache() 判断请求头中 cache-control 是否是 no-cache(表示使用协商缓存)
// hasConditions() 判断请求头中是否有 If-Modified-Since 与 If-None-Match
// 这两个也表示使用协商缓存
// 使用协商缓存就必须请求网络,所以 cacheResponse 为 null
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
// 下面就是为请求头添加上跟缓存相关的一些头
// 这一块是根据响应头中的 Age,Date 等判断缓存时间,涉及到各种时间的计算
CacheControl responseCaching = cacheResponse.cacheControl();
// 响应的年龄:响应从创建到现在为止的存活时间
long ageMillis = cacheResponseAge();
// 响应的最大年龄:即在创建后最 freshMillis 内它是有效的、新鲜的。可以理解为读取的是 cache-control 中设置的 max-age 值
long freshMillis = computeFreshnessLifetime();
// 请求设置的 max-age。
// 和服务端设置的有可能不一样,为了保证缓存有新鲜度,取两者小值
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 请求设置的 min-refresh:即客户要求返回的响应在该时间内应该是新鲜的
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 设置的 max-stale:即接收的最大过期时间
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.
// 根据缓存的响应头中的 ETag, Last-Modified 等为请求配置相应的请求头
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);
}
上面就是 getCandidate() 中的全部逻辑,总结下:除非缓存可用或者使用协商缓存,否则返回的 CacheStrategy 中 cacheResponse 都为 null;
上面【代码一】中有一些时间计算。举例说一下。
假设有一缓存,max-age = 10,客户能接收的 max-stale = 5,因此在缓存被创建后 15s 内客户是都能接受的。现在客户在 8s 的时候(以缓存创建时间为基准)去请求,min-refresh = 5,也就是说客户要求请求到数据后必须保证在 5s 内还是新鲜的,8+5 = 13,因此缓存只要在 13s 时还能保持新鲜即可。
但将 min-refresh=10,那么缓存就需要在 8+10=18s 时保持新鲜,很明显该缓存不满足条件,就不能使用缓存。
总结
上面就是 okhttp 整个关于缓存协议的实现。主要涉及到:If-None-Match,ETag 等相关的 http 头。
本地文件读取
缓存文件的读取是通过 InternalCache#get() 方法完成的,具体的实现是 Cache#get()
// Cache.java
@Nullable Response get(Request request) {
// 第一步:通过请求 url 生成 key
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
// 第二步:从 DiskLruCache 中读取
// cache 是 DiskLruCache 对象,不过是 okhttp 重写之后的
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
// 第三步:构造 Entry 对象。getSource 的参数为 0
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
// 第四步,返回 Response
Response response = entry.response(snapshot);
// 第五步,看缓存是否匹配
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
第一步不用说,就是对 url 进行 md5 得到字符串当 key。
第二步,它涉及到 DiskLruCache。此时只需要知道它会将缓存文件(一共有两个)读取到 snapshot 的 sources 属性中。该属性是一个长度为 2 的数组,每一个元素指向一个缓存文件
第三步,里面调用 getSource(),并传入参数 0。该方法返回的就是 snapshot.sources[0],也就是 key.0 对应的 Source 对象,通过该 Source 对象就可以读取缓存内容。
然后初始化了一个 Entry 对象,要注意:这个 Entry 是 Cache 的内部类,与下面的 DiskLruCache 使用的 Entry 不是同一个类。Entry 在构造时会解析该文件,下面是一个具体示例。
http://www.265.com/
GET
0
HTTP/1.1 200 OK
11
Accept-Ranges: bytes
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Last-Modified: Wed, 01 Sep 2021 04:03:21 GMT
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 0
Date: Thu, 02 Sep 2021 04:05:54 GMT
Transfer-Encoding: chunked
OkHttp-Sent-Millis: 1630555555814
OkHttp-Received-Millis: 1630555555950
// 如果是 https 下面还会有一部分 https 握手信息相关的内容
第四步,根据第三步解析的缓存信息构造 Response 对象。这一步会读取 key.1 也就是响应体缓存,并设置到 Response.body 字段中
第五步涉及主要内容:比较缓存的 url 与请求的 url 是否一样,比对请求方式是否一样,以及使用 Vary 字段进行验证
// Cache.Entry
public boolean matches(Request request, Response response) {
// url, requestMethod,varyHeaders 都是构造 Entry 对象时赋值的
// 可以理解为缓存的响应中的字段
return url.equals(request.url().toString())
&& requestMethod.equals(request.method())
&& HttpHeaders.varyMatches(response, varyHeaders, request);
}
// HttpHeaders.java
public static boolean varyMatches(
Response cachedResponse, Headers cachedRequest, Request newRequest) {
// varyFields() 作用是读取 Vary 字段,然后将值通过逗号分隔成集合
for (String field : varyFields(cachedResponse)) {
if (!equal(cachedRequest.values(field), newRequest.headers(field))) return false;
}
return true;
}
上面的 varyMatches() 就是根据缓存的响应头中的 Vary 字段对缓存的请求进行比对,如果有一个不相同说明缓存的文件对本次请求是无效的,所以就返回 false,进面导致 matches 返回 false,然后又导致 get() 返回 null。具体见 http 中关于 Vary 的讲解。
下面就是真正从本地文件读取了,涉及到的类是 DiskLruCache。
DiskLruCache
DiskLruCache 的构造在 Cache 类的构造函数中被调用的,因此使用 okhttp 的缓存时该类已经初始化完成。而且在该类的构造函数中会初始化一个线程池:只有一个非核心线程,且最大存活时间是 60s
它有几个辅助方法,先看看
readJournal
主要作用就是分析日志的每一行,然后创建 Entry 并保存到 lruEntries 中
private void readJournal() throws IOException {
// 读取缓存目录下的 journal 文件
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
// 然后根据文件一通操作,判断文件是否合法
int lineCount = 0;
while (true) {
try {
// 读取 journal 文件每一行
readJournalLine(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
// ...
} finally {
Util.closeQuietly(source);
}
}
// 这个方法就是分析日志的每一行
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(' ');
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
// 以 REMOVE 起头,表示删除相应的节点
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
// 创建 key 对应的 Entry 对象
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)) {
// 以 CLEAN 起头,表示操作完成,数据是正确的。CLEAN 的格式如下;
// CLEAN c7b41a3db7f97c47e06a3f4e7fbf636a 34 1503
// 这一步将后面两个数字给截取出来
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)) {
// 以 DIRTY 起头,表示脏数据,此时 readable 为 false
// DIRTY c7b41a3db7f97c47e06a3f4e7fbf636a
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// 以 READ 起头,表示只是一次读取,不需要做别的操作
// This work was already done by calling lruEntries.get().
} else {
throw new IOException("unexpected journal line: " + line);
}
}
里面涉及到 Entry 构造
Entry(String key) {
this.key = key;
// valueCount 值为 2
lengths = new long[valueCount];
cleanFiles = new File[valueCount];
dirtyFiles = new File[valueCount];
// The names are repetitive so re-use the same builder to avoid allocations.
StringBuilder fileBuilder = new StringBuilder(key).append('.');
int truncateTo = fileBuilder.length();
for (int i = 0; i < valueCount; i++) {
// 依次为 cleanFiles 与 dirtyFiles 填充数据
// CLEAN 中每一个 File 的文件名是: key.0 或者 key.1
fileBuilder.append(i);
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
// dirty 中文件名是 key.0.tmp 或者 key.1.tmp
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}
processJournal
遍历 readJournal 中填充的 lruEnties
private void processJournal() throws IOException {
fileSystem.delete(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
// 从 readJournal 中可知道,CLEAN 操作时该 if 判断成立
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
}
} else {
// 其余操作走 else,比如 DIRTY。这里就清除相应的缓存文件
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
fileSystem.delete(entry.cleanFiles[t]);
fileSystem.delete(entry.dirtyFiles[t]);
}
i.remove();
}
}
}
get
有了上面的 readJournal() 与 processJournal() 后 get() 逻辑就很简单了:
- 如果未初始化,首先会读取 journal 文件( DiskLruCache 的日志文件),根据日志文件填充 lruEntries 对象(LinkedHashMap 实例)
- 从 lruEntries 根据 key 返回值
- 向日志中记录一条 READ 记录
// DiskLruCache.java
public synchronized Snapshot get(String key) throws IOException {
// 第一步:处理日志文件。就是调用 readJournal() 与 processJournal()
initialize();
checkNotClosed();
validateKey(key);
// lruEntries 是在第一步中填充的数据
Entry entry = lruEntries.get(key);
// entry.readable 只有在 CLEAN 行时才为真
if (entry == null || !entry.readable) return null;
// 第二步:根据 entry 获取缓存文件的快照
Snapshot snapshot = entry.snapshot();
if (snapshot == null) return null;
//第三步:记录一个 READ 日志,格式为 *READ c7b41a3db7f97c47e06a3f4e7fbf636a*
redundantOpCount++;
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n'
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
// 返回
return snapshot;
}
Snapshot snapshot() {
// valueCount 值为 2
Source[] sources = new Source[valueCount];
// 值是两个数字
long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
try {
// 将 key.0 与 key.1 两个文件转为 Source 对象,可以理解为读取文件
for (int i = 0; i < valueCount; i++) {
sources[i] = fileSystem.source(cleanFiles[i]);
}
return new Snapshot(key, sequenceNumber, sources, lengths);
} catch (FileNotFoundException e) {
return null;
}
}
get() 看完后,Cache.get() 中第二步就相当于分析完了,它拿到的 snapshot 就是上面 snapshot() 方法的返回值,其字段 sources 代表本地文件 key.0 与 key.1
总结
到目前为止,缓存的读是已经结束了,总结下:
- 只缓存响应,并且将响应头与响应体缓存到两个文件中,前者用 key.0,后者用 key.1
本地缓存写
入口在 CacheInterceptor.intercept(),在执行完网络请求后,会判断是否可进行缓存,如果可以就调用 Cache.put() 存储响应
// Cache.java
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
// 首先判断是否可进行缓存,非 GET 请求或者 Vary 字段中包含 * 号,就会直接返回。下面的代码就执行不到
// 构造 Entry 对象,这里面解析一些响应头,比如返回的状态码、握手信息等
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
// cache 为 DiskLruCache 实例
// 这里返回一个 editor 实例。它 close 的时候会同时将
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
// 上面说过 entry 在构造时会解析一些响应头,这里就是将响应头等信息写入到 key.0.tmp 文件中
entry.writeTo(editor);
// 返回 CacheRequestImpl 实例,它才是将数据真正写入到缓存
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
// 注意这里返回的 sink,它指向的是 tmp 文件
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
// 省略各种头信息的写入
}
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
// 获取 dirty 文件,即 tmp 文件
File dirtyFile = entry.dirtyFiles[index];
Sink sink;
try {
sink = fileSystem.sink(dirtyFile);
} catch (FileNotFoundException e) {
return Okio.blackhole();
}
return new FaultHidingSink(sink) {
@Override protected void onException(IOException e) {
synchronized (DiskLruCache.this) {
detach();
}
}
};
}
}
上面的代码可以发现:
- 并没有写入响应体,而响应体的写入是在 CacheRequestImpl 中完成
- 先向 tmp 文件中写入
DiskLruCache.edit
主要作用:
- 向日志文件中记录 DIRTY 行
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
// 这一步在 get() 时说过,就是处理日志文件(journal 文件)
initialize();
// ...
// Flush the journal before creating files to prevent file leaks.
// 向日志文件中记录 DIRTY 行
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
if (entry == null) {
// 这个 Entry 是 DiskLruCache 内部类
// 这个构造函数上面也说过,它会生成 key.0 与 key.1 两个文件对应的 File 对象
entry = new Entry(key);
lruEntries.put(key, entry);
}
// 新建一个 Editor 返回
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
CacheRequestImpl
Cache.put() 时会创建 CacheRequestImpl 的实例
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
// 这里返回的是 key.1.tmp 文件对应的 Sink,也是先写入到 tmp 文件
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();
// 会将 key.0|1.tmp 重命名为 key.0|1
editor.commit();
}
};
}
可以在 close() 处打个断点,到执行到 close 时,缓存目录中只有 tmp 文件。最后面执行的 editor.commit() 才会真正的将缓存落地。
上面的 commit() 最终会调用到 completeEdit:
synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
// 省略各种判断
for (int i = 0; i < valueCount; i++) {
File dirty = entry.dirtyFiles[i];
if (success) {
if (fileSystem.exists(dirty)) {
File clean = entry.cleanFiles[i];
// 将 dirty 文件重命名为 clean 文件
fileSystem.rename(dirty, clean);
long oldLength = entry.lengths[i];
long newLength = fileSystem.size(clean);
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
fileSystem.delete(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
// 然后向 journal 文件中写入 CLEAN 行
journalWriter.writeUtf8(CLEAN).writeByte(' ');
journalWriter.writeUtf8(entry.key);
entry.writeLengths(journalWriter);
journalWriter.writeByte('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.writeUtf8(REMOVE).writeByte(' ');
journalWriter.writeUtf8(entry.key);
journalWriter.writeByte('\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
}
这里只看到将 tmp 文件重命名为正常的缓存文件,没看到哪个地方往 tmp 文件中写入数据。不过,也不重要。