OkHttp3.0解析——谈谈内部的缓存策略

2,313 阅读5分钟

前言

合理的利用本地的缓存策略,可以有效的减少网络请求时候的网络开销,减少响应的延迟。而在OkHttp3.0中的缓存主要作用在缓存拦截器CacheInterceptor里面。所以现在我们就具体分析下CacheInterceptor中对缓存的具体操作。

CacheInterceptor

我们都知道,OkHttp的核心或者说精华部分就是其强大的拦截器功能,几乎你在使用他的时候都是一些拦截器在背后默默帮你做一些操作。而缓存拦截器也正是在背后默默帮你对数据的缓存作着操作。在了解缓存拦截器之前,我们必须先理解内部的三个东西。

Cache: 缓存管理器。其内部拥有一个DiskLruCache算法在操作,将获取到的缓存写入到系统文件当中去。

CacheStrategy: 缓存策略。内部维护了request与response。通过策略来判断到底是从网络端获取数据还是从本地缓存中获取数据亦或者两者并用。

CacheStrategyFactory: 缓存工厂。通过此方法来获取到缓存策略这个对象。

实际的缓存是在CacheInterceptor这个类中的intercept方法中完成的,那么我们下面来看看这个方法中具体的操作逻辑。

  @Override public Response intercept(Chain chain) throws IOException {

    //先去获取缓存
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //从缓存策略工厂中获取缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    //如果当前的缓存不符合要求,则将其close
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // 如果网络不能用并且缓存不能用则抛出504服务器超时异常
    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());
      }
    }

    // 如果既有缓存,同时又发起了请求,说明此时是一个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());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {

        // 将网络请求之后的结果写入cache
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

分析上面代码可以看到,首先我们会从缓存策略工厂(CacheStrategyFactory)中获取缓存策略(CacheStrategyFactory)。之后做几次判断,如果本地有缓存则直接获取缓存,如果缓存和网络都不能使用,则抛出504连接超时的异常。如果本地没有缓存但是网络可以使用,则调用networkResponse来请求网络数据,并且将网络数据通过cacheWritingResponse()写入diskLruCache中。到此整个缓存就算是全部弄完了。

DiskLruCache:

Cache内部通过DiskLruCache管理cache在文件系统层面的创建,读取,清理等等工作,接下来看下DiskLruCache的主要逻辑:

public final class DiskLruCache implements Closeable, Flushable {
  
  final FileSystem fileSystem;
  final File directory;
  private final File journalFile;
  private final File journalFileTmp;
  private final File journalFileBackup;
  private final int appVersion;
  private long maxSize;
  final int valueCount;
  private long size = 0;
  BufferedSink journalWriter;
  final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);

  // Must be read and written when synchronized on 'this'.
  boolean initialized;
  boolean closed;
  boolean mostRecentTrimFailed;
  boolean mostRecentRebuildFailed;

  /**
   * To differentiate between old and current snapshots, each entry is given a sequence number each
   * time an edit is committed. A snapshot is stale if its sequence number is not equal to its
   * entry's sequence number.
   */
  private long nextSequenceNumber = 0;

  /** Used to run 'cleanupRunnable' for journal rebuilds. */
  private final Executor executor;
  private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
        ......
    }
  };
  ...
  }

DiskLruCache内部日志文件,对cache的每一次读写都对应一条日志记录,DiskLruCache通过分析日志分析和创建cache

日志文件的应用场景主要有四个:

  • DiskCacheLru初始化时通过读取日志文件创建cache容器:lruEntries。同时通过日志过滤操作不成功的cache项。相关逻辑在DiskLruCache.readJournalLine,DiskLruCache.processJournal
  • 初始化完成后,为避免日志文件不断膨胀,对日志进行重建精简,具体逻辑在DiskLruCache.rebuildJournal
  • 每当有cache操作时将其记录入日志文件中以备下次初始化时使用
  • 当冗余日志过多时,通过调用cleanUpRunnable线程重建日志

每一个DiskLruCache.Entry对应一个cache记录

一个Entry主要由以下几部分构成:

  • key:每个cache都有一个key作为其标识符。当前cache的key为其对应URL的MD5字符串
  • cleanFiles/dirtyFiles:每一个Entry对应多个文件,其对应的文件数由DiskLruCache.valueCount指定。当前在OkHttp中valueCount为2。即每个cache对应2个cleanFiles,2个dirtyFiles。其中第一个cleanFiles/dirtyFiles记录cache的meta数据(如URL,创建时间,SSL握手记录等等),第二个文件记录cache的真正内容。cleanFiles记录处于稳定状态的cache结果,dirtyFiles记录处于创建或更新状态的cache
  • currentEditor:entry编辑器,对entry的所有操作都是通过其编辑器完成。编辑器内部添加了同步锁

总结

总结起来DiskLruCache主要有以下几个特点:

  • 通过LinkedHashMap实现LRU替换
  • 通过本地维护Cache操作日志保证Cache原子性与可用性,同时为防止日志过分膨胀定时执行日志精简
  • 每一个Cache项对应两个状态副本:DIRTY,CLEAN。CLEAN表示当前可用状态Cache,外部访问到的cache快照均为CLEAN状态;DIRTY为更新态Cache。由于更新和创建都只操作DIRTY状态副本,实现了Cache的读写分离
  • 每一个Cache项有四个文件,两个状态(DIRTY,CLEAN),每个状态对应两个文件:一个文件存储Cache meta数据,一个文件存储Cache内容数据

有兴趣可以关注我的小专栏,学习更多职场产品思考知识:小专栏