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

栏目: Java · 发布时间: 6年前

内容简介:合理的利用本地的缓存策略,可以有效的减少网络请求时候的网络开销,减少响应的延迟。而在OkHttp3.0中的缓存主要作用在缓存拦截器CacheInterceptor里面。所以现在我们就具体分析下CacheInterceptor中对缓存的具体操作。我们都知道,OkHttp的核心或者说精华部分就是其强大的拦截器功能,几乎你在使用他的时候都是一些拦截器在背后默默帮你做一些操作。而缓存拦截器也正是在背后默默帮你对数据的缓存作着操作。在了解缓存拦截器之前,我们必须先理解内部的三个东西。Cache:缓存管理器。其内部拥

合理的利用本地的缓存策略,可以有效的减少网络请求时候的网络开销,减少响应的延迟。而在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内容数据
OkHttp3.0解析——谈谈内部的缓存策略

以上所述就是小编给大家介绍的《OkHttp3.0解析——谈谈内部的缓存策略》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

挑战程序设计竞赛

挑战程序设计竞赛

秋叶拓哉、岩田阳一、北川宜稔 / 巫泽俊、庄俊元、李津羽 / 人民邮电出版社 / 2013-7-1 / CNY 79.00

世界顶级程序设计高手的经验总结 【ACM-ICPC全球总冠军】巫泽俊主译 日本ACM-ICPC参赛者人手一册 本书对程序设计竞赛中的基础算法和经典问题进行了汇总,分为准备篇、初级篇、中级篇与高级篇4章。作者结合自己丰富的参赛经验,对严格筛选的110 多道各类试题进行了由浅入深、由易及难的细致讲解,并介绍了许多实用技巧。每章后附有习题,供读者练习,巩固所学。 本书适合程序设计......一起来看看 《挑战程序设计竞赛》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具