Okhttp的缓存机制

416 阅读8分钟

Okhttp的缓存机制

Http协议下的缓存机制

强制缓存

通过http协议所传送的数据,会被保存到缓存数据库中,强制缓存的意思是指,若缓存数据库中的数据仍未失效,则直接通过缓存数据库获得数据,不再通过http向服务器发送请求。其中有两个比较重要的字段用于控制是否失效:

Expires

指过期的时间,其值由服务器所决定。当在缓存数据库取得相应的数据时,通过比较当前时间与Expires来决定是否直接使用缓存数据库中的数据。然而值得注意的是,服务端和客户端之间存在着延时,没有统一的时间标准,因此随着Http协议的发展,使用的机会也就越来越少

Cache-Control

字面理解为缓存的控制,其实际意思是缓存的属性。类似于java中的作用域,http中的缓存分为如下几种类型:

  1. public:表示其中的数据完全可以被存储,包括密码等隐私信息,且所有人就可以访问,其安全性也较低
  2. private: 存储到用户的私有cache中去,只有用户本身可以访问(默认)
  3. no-cache 仅在客户端与服务端建立认证后,才可以缓存(用于对比缓存)
  4. no-store 代表其中的请求和响应等信息都不会被缓存
  5. max-age 表示所返回的数据已经过期或失效

对比缓存

使用前要与服务器的缓存进行对比。通过服务器返回的状态码决定是否使用

304: 使用对比缓存的数据

200:使用服务器的最新数据

判定字段

Etag

资源的唯一标识,类似于人们的身份证号码,资源的内容一旦发生改动,Etag就会发生改变。客户端发送请求的时候格式为 If-None-Match +Etag,服务器收到后则会与缓存的Etag进行比对

Last-Modified

字面意思,最近修改的时间,由服务器所决定,客户端在发送请求的时候使用If-Modified-Since + 指定时间,若客户端所存的数据≤该时间则说明资源没有改动

To put into a nutshell :

Okhttp的缓存机制

可以看到,http本身协议的缓存机制较为简单,不能很好的满足实际的需求。okhttp相对来说便更加复杂。先说结论:

  1. 缓存基于文件存储
  2. 内部维护基于LRU算法的缓存清理线程

Okhttp读取缓存流程

Okhttp 存储缓存流程

源码解析

CacheControl

用于指定缓存的规则

public final class CacheControl {

  //表示这是一个优先使用网络验证,验证通过之后才可以使用缓存的缓存控制,设置了noCache
  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  //表示这是一个优先先使用缓存的缓存控制,设置了onlyIfCached和maxStale的最大值
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();

  //以下的字段都是HTTP中Cache-Control字段相关的值
  private final boolean noCache;
  private final boolean noStore;
  private final int maxAgeSeconds;
  private final int sMaxAgeSeconds;
  private final boolean isPrivate;
  private final boolean isPublic;
  private final boolean mustRevalidate;
  private final int maxStaleSeconds;
  private final int minFreshSeconds;
  private final boolean onlyIfCached;
  private final boolean noTransform;

  //解析头文件中的相关字段,得到该缓存控制类
  public static CacheControl parse(Headers headers) {
    ...
  }

}

CacheStrategy

主要用于判断是否使用缓存数据

public final class CacheStrategy {

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      //网络请求和缓存响应
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        //找到缓存响应的响应头信息
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          //查看响应头信息中是否有以下字段信息
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

    public CacheStrategy get() {
      //获取判定的缓存策略
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // 如果判定的缓存策略的网络请求不为空,但是只使用缓存,则返回两者都为空的缓存策略。
        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.
      //如果请求是https,而缓存响应的握手信息为空,则返回没有缓存响应的策略
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

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

      //获取请求头中的CacheControl信息
      CacheControl requestCaching = request.cacheControl();
      //如果请求头中的CacheControl信息是不缓存的,则返回没有缓存响应的策略
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      //获取响应的年龄
      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信息
      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());
      }

      //构造一个新的有条件的Request,添加If-None-Match,If-Modified-Since等信息
      Request.Builder conditionalRequestBuilder = request.newBuilder();

      if (etag != null) {
        conditionalRequestBuilder.header("If-None-Match", etag);
      } else if (lastModified != null) {
        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
      } else if (servedDate != null) {
        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
      }

      Request conditionalRequest = conditionalRequestBuilder.build();
      //根据是否有If-None-Match,If-Modified-Since信息,返回不同的缓存策略
      return hasConditions(conditionalRequest)
          ? new CacheStrategy(conditionalRequest, cacheResponse)
          : new CacheStrategy(conditionalRequest, null);
    }

    /**
     * Returns true if the request contains conditions that save the server from sending a response
     * that the client has locally. When a request is enqueued with its own conditions, the built-in
     * response cache won't be used.
     */
    private static boolean hasConditions(Request request) {
      return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
    }
}

Cache

对外开放的缓存类,类似数据库能够增删改查

  1. 增添缓存
CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    //如果请求是"POST","PUT","PATCH","PROPPATCH","REPORT"则移除这些缓存  
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
      }
      return null;
    }
    //仅支持GET的请求缓存,其他请求不缓存
    if (!requestMethod.equals("GET")) {
       return null;
    }
    //判断请求中的http数据包中headers是否有符号"*"的通配符,有则不缓存  
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    //把response构建成一个Entry对象
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //生成DiskLruCache.Editor对象
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //对缓存进行写入
      entry.writeTo(editor);
      //构建一个CacheRequestImpl类,包含Ok.io的Sink对象
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

  1. 查找缓存
Response get(Request request) {
    //获取url转换过来的key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
         //根据key获取对应的snapshot 
         snapshot = cache.get(key);
         if (snapshot == null) {
             return null;
         }
    } catch (IOException e) {
      return null;
    }
    try {
     //创建一个Entry对象,并由snapshot.getSource()获取Sink
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    //通过entry和response生成respson,通过Okio.buffer获取请求体,然后封装各种请求信息
    Response response = entry.response(snapshot);
    if (!entry.matches(request, response)) {
      //对request和Response进行比配检查,成功则返回该Response。
      Util.closeQuietly(response.body());
      return null;
    }
    return response;
  }

  1. 更新缓存
void update(Response cached, Response network) {
    //用Respon构建一个Entry
    Entry entry = new Entry(network);
    //从缓存中获取DiskLruCache.Snapshot
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      //获取DiskLruCache.Snapshot.edit对象
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        //将entry写入editor中
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

  1. 删除缓存

主体位于DiskLruCache之中

void remove(Request request) throws IOException {
    //通过url转化成的key去删除缓存
    cache.remove(key(request.url()));
  }

  1. writeTo ok.io
 public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url)
          .writeByte('\\n');
      sink.writeUtf8(requestMethod)
          .writeByte('\\n');
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(varyHeaders.value(i))
            .writeByte('\\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString())
          .writeByte('\\n');
      sink.writeDecimalLong(responseHeaders.size() + 2)
          .writeByte('\\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(responseHeaders.value(i))
            .writeByte('\\n');
      }
      sink.writeUtf8(SENT_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(sentRequestMillis)
          .writeByte('\\n');
      sink.writeUtf8(RECEIVED_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(receivedResponseMillis)
          .writeByte('\\n');

      if (isHttps()) {
        sink.writeByte('\\n');
        sink.writeUtf8(handshake.cipherSuite().javaName())
            .writeByte('\\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\\n');
      }
      sink.close();
    }

DiskLruCache

真实存储(文件格式)的缓存功能类,使用了基于LinkedHashedMap。可以看到除了一些关键的方法之外其主要包括了三个重要的内部类。

  1. Entry

用于存储缓存数据的实体类,一个url对应一个实体,在Entry还有Snapshot对象

private final class Entry {
  final String key;

  /** Lengths of this entry's files. */
  final long[] lengths;
  final File[] cleanFiles;
  final File[] dirtyFiles;

  /** True if this entry has ever been published. */
  boolean readable;

  /** The ongoing edit or null if this entry is not being edited. */
  Editor currentEditor;

  /** The sequence number of the most recently committed edit to this entry. */
  long sequenceNumber;

  Entry(String key) {
    this.key = key;

    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++) {
      fileBuilder.append(i);
      cleanFiles[i] = new File(directory, fileBuilder.toString());
      fileBuilder.append(".tmp");
      dirtyFiles[i] = new File(directory, fileBuilder.toString());
      fileBuilder.setLength(truncateTo);
    }
  }

  /** Set lengths using decimal numbers like "10123". */
  void setLengths(String[] strings) throws IOException {
    if (strings.length != valueCount) {
      throw invalidLengths(strings);
    }

    try {
      for (int i = 0; i < strings.length; i++) {
        lengths[i] = Long.parseLong(strings[i]);
      }
    } catch (NumberFormatException e) {
      throw invalidLengths(strings);
    }
  }

  /** Append space-prefixed lengths to {@code writer}. */
  void writeLengths(BufferedSink writer) throws IOException {
    for (long length : lengths) {
      writer.writeByte(' ').writeDecimalLong(length);
    }
  }

  private IOException invalidLengths(String[] strings) throws IOException {
    throw new IOException("unexpected journal line: " + Arrays.toString(strings));
  }

  /**
   * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
   * single published snapshot. If we opened streams lazily then the streams could come from
   * different edits.
   */
  Snapshot snapshot() {
    if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();

    Source[] sources = new Source[valueCount];
    long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
    try {
      for (int i = 0; i < valueCount; i++) {
        sources[i] = fileSystem.source(cleanFiles[i]);
      }
      return new Snapshot(key, sequenceNumber, sources, lengths);
    } catch (FileNotFoundException e) {
      // A file must have been deleted manually!
      for (int i = 0; i < valueCount; i++) {
        if (sources[i] != null) {
          Util.closeQuietly(sources[i]);
        } else {
          break;
        }
      }
      // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
      // size.)
      try {
        removeEntry(this);
      } catch (IOException ignored) {
      }
      return null;
    }
  }
}

  1. Snapshot
public final class Snapshot implements Closeable {
    private final String key;
    private final long sequenceNumber;
    private final Source[] sources;
    private final long[] lengths;

    Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
      this.key = key;
      this.sequenceNumber = sequenceNumber;
      this.sources = sources;
      this.lengths = lengths;
    }

    public String key() {
      return key;
    }

    /**
     * Returns an editor for this snapshot's entry, or null if either the entry has changed since
     * this snapshot was created or if another edit is in progress.
     */
    public @Nullable Editor edit() throws IOException {
      return DiskLruCache.this.edit(key, sequenceNumber);
    }

    /** Returns the unbuffered stream with the value for {@code index}. */
    public Source getSource(int index) {
      return sources[index];
    }

    /** Returns the byte length of the value for {@code index}. */
    public long getLength(int index) {
      return lengths[index];
    }

    public void close() {
      for (Source in : sources) {
        Util.closeQuietly(in);
      }
    }
  }

  1. Editor

在Editor的初始化中要传入Editor,其实Editor就是编辑entry的类

public final class Editor {
  final Entry entry;
  final boolean[] written;
  private boolean done;

  Editor(Entry entry) {
    this.entry = entry;
    this.written = (entry.readable) ? null : new boolean[valueCount];
  }

  /**
   * Prevents this editor from completing normally. This is necessary either when the edit causes
   * an I/O error, or if the target entry is evicted while this editor is active. In either case
   * we delete the editor's created files and prevent new files from being created. Note that once
   * an editor has been detached it is possible for another editor to edit the entry.
   */
  void detach() {
    if (entry.currentEditor == this) {
      for (int i = 0; i < valueCount; i++) {
        try {
          fileSystem.delete(entry.dirtyFiles[i]);
        } catch (IOException e) {
          // This file is potentially leaked. Not much we can do about that.
        }
      }
      entry.currentEditor = null;
    }
  }

  /**
   * Returns an unbuffered input stream to read the last committed value, or null if no value has
   * been committed.
   */
  public Source newSource(int index) {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (!entry.readable || entry.currentEditor != this) {
        return null;
      }
      try {
        return fileSystem.source(entry.cleanFiles[index]);
      } catch (FileNotFoundException e) {
        return null;
      }
    }
  }

  /**
   * Returns a new unbuffered output stream to write the value at {@code index}. If the underlying
   * output stream encounters errors when writing to the filesystem, this edit will be aborted
   * when {@link #commit} is called. The returned output stream does not throw IOExceptions.
   */
  public Sink newSink(int index) {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (entry.currentEditor != this) {
        return Okio.blackhole();
      }
      if (!entry.readable) {
        written[index] = true;
      }
      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();
          }
        }
      };
    }
  }

  /**
   * Commits this edit so it is visible to readers.  This releases the edit lock so another edit
   * may be started on the same key.
   */
  public void commit() throws IOException {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (entry.currentEditor == this) {
        completeEdit(this, true);
      }
      done = true;
    }
  }

  /**
   * Aborts this edit. This releases the edit lock so another edit may be started on the same
   * key.
   */
  public void abort() throws IOException {
    synchronized (DiskLruCache.this) {
      if (done) {
        throw new IllegalStateException();
      }
      if (entry.currentEditor == this) {
        completeEdit(this, false);
      }
      done = true;
    }
  }

  public void abortUnlessCommitted() {
    synchronized (DiskLruCache.this) {
      if (!done && entry.currentEditor == this) {
        try {
          completeEdit(this, false);
        } catch (IOException ignored) {
        }
      }
    }
  }
}

  1. 删除
boolean removeEntry(Entry entry) throws IOException {
  if (entry.currentEditor != null) {
    entry.currentEditor.detach(); // Prevent the edit from completing normally.
  }

  for (int i = 0; i < valueCount; i++) {
    fileSystem.delete(entry.cleanFiles[i]);
    size -= entry.lengths[i];
    entry.lengths[i] = 0;
  }

  redundantOpCount++;
  journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\\n');
  lruEntries.remove(entry.key);

  if (journalRebuildRequired()) {
    executor.execute(cleanupRunnable);
  }

  return true;
}