okHttp缓存机制

基本流程

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实现。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值