okhttp 版本:com.squareup.okhttp3:okhttp:3.12.1
本文存在背景
笔者之前对 okhttp 缓存机制不太了解,分析 okhttp 网络请求过程时,对此也是一笔带过,认为缓存这块与请求过程无太大关系,所以一直以来,对缓存这块不得而知,决定好好阅读下源码。
网上介绍 okhttp 缓存机制文章大部分都是分两部分介绍:
- okhttp 是如何实现 http 缓存机制的
- DiskLruCache源码解析
本文不涉及 http 缓存机制,主要看本地缓存源码解析(即数据流是如何写到本地的),笔者在看的过程中产生了一个疑问:从网络返回的数据响应头,即 Header 存储到本地可以清楚的看到调用地方,通过Cache.put(),但这也只是响应头而已,响应体(body)呢?虽然知道是在 CacheInterceptor.cacheWritingResponse() 中写入的,这里贴一下代码,方便说明:
--> CacheInterceptor.java
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;
}
// 虽然知道最后肯定是通过这里写到本地的,但是read方法没看到有地方调用
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, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};
String contentType = response.header("Content-Type");
long contentLength = response.body().contentLength();
// cacheWritingSource 的 read 方法要被调用,才能将body写到本地,但是这里
// 只是将 cacheWritingSource 包装了下,并赋值给了 RealResponseBody 成员变量
return response.newBuilder()
.body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
.build();
}
如上,中文注释表述了疑问,不知道 cacheWritingSource 的 read 方法在哪里被调用了,继续往上游的拦截器 BridgeInterceptor、RetryAndFollowUpInterceptor寻找,看看在数据返回后,有没有调用的地方,依然没有找到,决心debug,看看调用 read 方法的调用栈到底是什么。
debug失败
本想着,既然要将数据流缓存到本地,cacheWritingSource 的 read 方法肯定是要走的,那么就在 read 方法 中打个断点,等执行到断点时,顺便看下此时的调用栈,不就清楚了?
然而,断点没走到!!
没走 read 方法,那数据流是怎么存到本地的?难道压根就没有缓存到本地?cd到缓存目录下看一看:
除了journal文件,只看到了两个 .tmp 文件,简要说明一下 .tmp 文件:
DiskLruCache 中 有个内部类 Entry,每个 Entry 对象对应一个 url,被存储在 LinkedHashMap 中,Entry 中有四个文件,分别为:
- key.0 (最终保存Header的文件)
- key.0.tmp (临时保存Header的文件)
- key.1 (最终保存Body的文件)
- key.1.tmp (临时保存Body的文件) 其中 key 是对 url 进行 md5 后得到的值,虽然有四个文件,在数据流写入本地的时候,都是先操作key.0.tmp,key.1.tmp,当数据流写完后,关闭流时,会将 key.0.tmp,key.1.tmp 重命名为 key.0,key.1。
继续回来,目前我们只看到了两个 tmp 文件,按照上面说的,当数据写完后,会将 tmp 文件重命名key.0,key.1,为什么没看到?说明数据流根本没往里写,来看下这两个文件的内容:
-->保存 Header 的 key.0.tmp 文件
-->保存 Body 的 key.1.tmp 文件
分析一下上面两张图,第一张是保存网络返回数据的 Header,第二张是网络返回数据的 body,发现第二张图是空的,也就是 body 数据没有写到 key.1.tmp 文件中,那为啥第一张图有数据呢?因为在Cache.put()时就已经将数据写入了。
到现在为止,知道了数据流确实没有写入到本地。
真相
请求的 url 确定是要缓存的,Header 已经写入到了本地,但是 Body 没有被写入到本地,怀疑是不是网络返回数据 Body 为空,来把请求的数据显示一下,看有没有数据,这里是请求的一张图片:
没问题,图片能显示出来,再cd到缓存目录看,居然变成了 key.0,key.1:
顿时明白了,这里才是源头,这里调用了 response.body().bytes(),这里面就会去读取数据,然后最开始那张图里的内部类的 read 方法才能被调用到,后面才会将流写到本地。
原来,只有主动去读取了数据,才能触发数据流的写入,这也就解释了本文的疑问,确实没有地方调用读取流的方法,需要我们自己去调用读取
试想,网络数据返回后,不去读取,也就没必要去缓存到本地了,因为你根本没用到这个数据。
最后,贴一张 Source 包含关系图(前提是当前 url 本地无缓存):