OkHttp3源码分析之缓存Cache

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aiynmimi/article/details/79807036

前言

网络请求在一个应用中的使用场景是非常多且频繁的,那么每次与服务器进行数据交互都去进行网络请求的话,会大大增大应用响应时间,最重要的非常浪费流量,所以缓存就特别重要了,相信大部分做开发的同学都比较熟悉这个过程了,每次需要数据交互的时候,先从本地/内存缓存读取,如果没有再去远程进行网络请求,并将其加入缓存中,一些比较知名的涉及到网络请求的框架一般都会有相应的缓存处理,有的不仅有二级缓存,还有三级缓存,比如Glide等,更有甚者称其有四级缓存,如Fresco(当然本质上还是三级缓存)。那么OkHttp这个本身就是一个网络请求的框架也有自己的缓存处理,本篇文章就来深入了解一下它的缓存原理。

准备知识

我们知道发起一个网络请求,与服务器进行交互,就是HTTP协议的相关内容,那么这里缓存也就是HTTP协议的缓存机制。
这部分内容呢,我查阅了一些资料或者博客,下面这篇博客总结的非常好,建议先了解一下:
浏览器 HTTP 协议缓存机制详解

使用

在了解OkHttp的缓存原理之前,首先我们得知道怎么用它吧!

//缓存文件夹
File cacheFile = new File(getExternalCacheDir().toString(),"MyCache");
//缓存大小为10M
int cacheSize = 10 * 1024 * 1024;
//创建缓存对象
Cache cache = new Cache(cacheFile,cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
        .cache(cache)
        .build();

OkHttp默认是不开启缓存的,要使用缓存也非常简单。
创建一个要存放缓存的文件夹,设置缓存的大小,然后构建一个Cache对象,然后在创建OkHttpClient对象的时候,使用Builder(),调用cache()方法,传入构建好的Cache对象即可。

关于最简单的使用,就到这里,更详细的可以参考下面这篇文章:
OKHTTP之缓存配置详解

分析

源码基于最新的版本:3.10.0。

我们在开启缓存的时候,就一句cache(cache)就OK了,那么它怎么就缓存了呢?下边我们来看一下。

这就要回到我们前边拦截器的部分了,我们在发起一个请求后,在运行到第三个核心拦截器,也就是执行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);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    //禁止使用网络(根据缓存策略),缓存又无效,直接返回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 we don't need the network, we're done.
    //如果有缓存同时又不使用网络,则直接返回缓存结果
    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.
    // 如果既有缓存,同时又发起了请求,要做出选择
    if (cacheResponse != null) {
      // 如果服务端返回的是NOT_MODIFIED,缓存有效,将本地缓存和网络响应做合并
      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)) {
        // Offer this request to the cache.
        // 将网络响应写入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;
  }

首先是尝试先从cache中获取:

final InternalCache cache;

public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
}

Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

源码中cacheInternalCache的一个实例对象,InternalCache是OkHttp的一个内部接口,那既然InternalCache是一个接口,那这个cache对象到底是谁?我们又要回到之前,在构建拦截器链的时候RealCall#getResponseWithInterceptorChain()

Response getResponseWithInterceptorChain() throws IOException {
    ...
    interceptors.add(new CacheInterceptor(client.internalCache()));
    ...
}

可以看到在创建CacheInterceptor时,构造方法中传入了client.internalCache(),那么看一下:

InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
}

看到这里就明白了,还记得我们开头创建OkHttpClient对象时,调用的.cache(cache)吗?因为我们创建了一个Cache对象,所以这里的cache不为空,然后CacheInterceptor构造方法中传入的就是这里return的cache.internalCache,那么继续:
Cache.internalCache

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

哇,终于发现了在最开始尝试获取缓存时的cache,其实就是创建OkHttpClient时传入的Cache对象的internalCache字段,同时internalCache实现的接口的全部方法实际都交由Cache做具体的处理。

好吧,饶了这么大一圈,回到开头继续吧!

我们通过上边的分析,知道这里cache不为null,那么调用cache.get(chain.request())
看一下Cache#get()方法:

@Nullable 
Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }

    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

首先key(request.url()),根据请求的url,得到一个字符串key(md5,hex),接着根据key,得到一个DiskLruCache.Snapshot快照,这里的cache就是一个DiskLruCache对象。

插句题外话DiskLruCache是jake大神的一个开源项目,用于做硬盘缓存,当然这里不是用的源项目,代码稍作了改动,但是原理都是一样的,这里不再赘述。

继续,从缓存快照中得到一个Source流,Source是Okio封装的一个高效的输入流,类似java.io.InputStream,然后由此创建一个Entry实体类。

EntryCache的一个内部类,根据文档描述,它从流里边读取出来的样子应该是这个样子:

http://google.com/foo
GET
2
Accept-Language: fr-CA
Accept-Charset: UTF-8
HTTP/1.1 200 OK
3
Content-Type: image/png
Content-Length: 100
Cache-Control: max-age=600

首先它们是由换行符分割的,也就是一个信息一行,前两行是请求的url(http://google.com/foo)和请求方法(GET),接着一个数字表示请求头Header的数目(2个)和具体的请求头(Accept-Language: fr-CAAccept-Charset: UTF-8),接着一行表示响应状态(HTTP/1.1 200 OK),然后一个数字表示响应头Header的数目(3个)和下边具体的响应头(Content-Type: image/png,Content-Length: 100,Cache-Control: max-age=600)。
如果它是一个HTTPS请求,那么在下边还会多一些SSL会话信息:

               //注意这里空一行
AES_256_WITH_MD5
2
base64-encoded peerCertificate[0]
base64-encoded peerCertificate[1]
-1
TLSv1.2

首先空出一行,接着一行加密套件信息(AES_256_WITH_MD5),接着是对等证书链的长度(2),这些证书是base64编码的,接下来就一行一个证书。再往下是本地证书链的长度,也是用base64编码的,然后也是一行一个证书。如果出现了长度(-1),就表示一个空数组,最后一行是可选的,如果有就表示TLS的版本(TLSv1.2)

而上边Entry的构造方法,就是在干上面这些事。

继续往下entry.response(snapshot);通过缓存快照得到一个Response实例,这块没什么好说的。
最后看从缓存中读取到的和当前请求是否一致,如果一致则返回Response,如果不一致则关闭。
Cache#matches()

public boolean matches(Request request, Response response) {
      return url.equals(request.url().toString())
          && requestMethod.equals(request.method())
          && HttpHeaders.varyMatches(response, varyHeaders, request);
    }

我的天哪,现在我们才终于从缓存中拿到cacheCandidate….,是不是都不记得cacheCandidate在哪了?哈哈哈哈…好吧,拿下来看一下,就是
CacheInterceptor#intercept()方法

Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

好了,继续往下,到了配置缓存策略这块:

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

如果上一步得到的cacheCandidate不为空,就配置缓存策略,这一步主要解析cacheCandidate中有关缓存的Header(Date\Expires\Last-Modified\ETag\Age)。对这些名词不熟悉的,请回到准备知识,阅读推荐的文章!
首先看一下工厂构造方法:

public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      //如果cacheResponse不为空,则开始解析
      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);
          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 = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

构造方式中主要的任务就是如果上一步得到的cacheCandidate不为空,则解析其中有关缓存的Header。
之后调用CacheStrategy.Factory#get()方法生成一个候选策略对象:

public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

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

它的主要实现在CacheStrategy.Factory#getCandidate()中:

private CacheStrategy getCandidate() {
      // No cached response.
      // 没有缓存响应,返回一个没有响应的策略,这里的cacheResponse就是我们在Factory构造方法中传入的
      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 requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      //下边一大段都是根据响应头Header,逐个判断,只有全部满足条件,才会返回该缓存策略
      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;
      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.
      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);
}

那么我们现在回过头来看一下CacheStrategy这个类,它的内部维护一个request和cacheResponse,经过一系列是否为空判断,缓存控制判断和一个个的响应头的判断来决定是否使用网络、缓存或两者都用。

插一句,这里出现了一个叫CacheControl的东西,这个类其实非常简单,它就是用来管理维护所有缓存相关的Header。其实在使用OkHttp进行缓存的时候,官方的推荐做法就是使用CacheControl,它是在每一个Request中进行设置,比如:

//设置缓存时间为60秒
CacheControl cacheControl = new CacheControl.Builder()
    .maxAge(60, TimeUnit.SECONDS)
    .build();
Request request = new Request.Builder()
    .url(URL)
    .cacheControl(cacheControl)
    .build();

它的使用也是非常简单,包括源码也是比较清晰的,这里就不再具体分析了。

好了,我们继续往下:cache.trackResponse(strategy);缓存监测,
来看一下Cache#trackResponse()方法

synchronized void trackResponse(CacheStrategy cacheStrategy) {
    requestCount++;

    if (cacheStrategy.networkRequest != null) {
      // If this is a conditional request, we'll increment hitCount if/when it hits.
      networkCount++;
    } else if (cacheStrategy.cacheResponse != null) {
      // This response uses the cache and not the network. That's a cache hit.
      networkCount++;
    }
  }

这里做的工作就是更新一下相关的统计指标requestCount,networkCount,networkCount

继续往下,如果从流中读取到的cacheCandidate不为空,但是根据缓存策略得到的cacheResponse为空,说明缓存不适用,关闭。

接着如果禁止使用网络(根据缓存策略),缓存又无效,直接返回504错误。这里其实对应的是CacheStrategy.Factory#get()中:

if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
}

然后往下,如果有缓存同时又不使用网络,则直接返回缓存结果。

接着,就是网络可用啊,就执行下一个拦截器获取响应。(这块可以参考前面关于拦截器的分析)。

然后,我们得到了网络请求到的响应,而此时缓存也可以用,那就要作判断了,代码中networkResponse.code() == HTTP_NOT_MODIFIED,如果网络请求到的结果和缓存相同,就把两个响应进行合并,然后执行update()将缓存更新。如果不相同,则关闭原有缓存。

最后,就剩一种情况了,就是只能使用网络了,然后将其保存在本地缓存中。下边来看一下如何保存的:

if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the 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.
        }
      }
    }

首先,cache依然是我们创建缓存目录,指定缓存大小之后创建的,肯定不为null,然后两个判断,这个得到的响应得有值而且可以被存储。
然后调用Cache#put()方法:

@Nullable 
CacheRequest put(Response response) {
    String requestMethod = response.request().method();

    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }

    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }

    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

先拿到请求方法,判断是否是无效的,在HttpMethod#invalidatesCache

public static boolean invalidatesCache(String method) {
    return method.equals("POST")
        || method.equals("PATCH")
        || method.equals("PUT")
        || method.equals("DELETE")
        || method.equals("MOVE");     // WebDAV
  }

可以看出有请求体的都不支持,然后该entry移除。
往下,不是GET请求,不缓存。按照注释中的说法:“我们在技术上允许缓存HEAD请求和一些POST请求,但是这样做的复杂性很高,收益也很低。”
然后head中包含*号的不缓存。
然后就是写入缓存了,这里跟读取缓存的时候一样,都是通过DiskLruCache实现的,这里也不再赘述。

参考文章:
关于Okhttp3(六)-CacheInterceptor
OkHttp 3.7源码分析(四)——缓存策略
OkHttp3源码分析[缓存策略]

猜你喜欢

转载自blog.csdn.net/aiynmimi/article/details/79807036