OkHttp 缓存

444 阅读10分钟

总结

  1. 不缓存 POST、HEAD、PUT 请求
  2. 缓存文件一共有两个,缓存文件名 key.0 与 key.1,前者存储响应头,后者存储响应体。其中 key 是 md5(url) 后的值。只缓存响应,不缓存请求
    • 另外,如果请求是 https,key.0 还会存储 https 握手相关的一些信息
  3. 只有读取返回的数据时,响应才会被缓存,并不是说拿到后台返回结果后立即就缓存到本地
    • 响应头不需要等到数据返回数据,它会首先缓存到 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();

主要涉及的类有

  1. CacheInterceptor:缓存拦截器,由该类触发缓存的所有操作,算是缓存的入口类
  2. CacheStrategy:缓存策略类。根据请求头、响应头(已有缓存的响应头)决定当前缓存是否可用。它就是对 http 协议中关于缓存部分的实现
  3. Cache:缓存存储类,内部使用 DiskLruCache 将缓存写入到硬盘文件中,也是外界传给 okhttp 内部的 Cache 对象
  4. InternalCache:CacheInterceptor 用于操作缓存的对象,它实际上是将所有操作都转给了 Cache 对象
  5. 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() 逻辑就很简单了:

  1. 如果未初始化,首先会读取 journal 文件( DiskLruCache 的日志文件),根据日志文件填充 lruEntries 对象(LinkedHashMap 实例)
  2. 从 lruEntries 根据 key 返回值
  3. 向日志中记录一条 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

总结

到目前为止,缓存的读是已经结束了,总结下:

  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();
        }
      }
    };
  }
}

上面的代码可以发现:

  1. 并没有写入响应体,而响应体的写入是在 CacheRequestImpl 中完成
  2. 先向 tmp 文件中写入

DiskLruCache.edit

主要作用:

  1. 向日志文件中记录 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 文件中写入数据。不过,也不重要。