okhttp 缓存机制--数据流是如何写到本地的

939 阅读4分钟

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 本地无缓存):