Android进阶之路Android技术知识Android开发

OkHttp源码解析 (四)——缓存机制

2019-08-03  本文已影响0人  码农翻身记

一、前言

OkHttp内置了缓存策略,在拦截器CacheInterceptor 中实现了缓存机制,默认情况不启用缓存,如果需要使用缓存,可以通过在OkHttpClient中设置全局缓存,或者对单个请求设置缓存。OkHttp的缓存机制遵循Http的缓存协议,因此,想要彻底理解OkHttp的缓存机制,则需要先了解Http的缓存协议的相关基础。可以参考 彻底弄懂HTTP缓存机制及原理。本文的基础介绍是根据该文总结。

二、Http缓存协议基础

根据是否需要与服务器交互可将缓存规则分为两类:
● 强制缓存
● 对比缓存
强制缓存优先级高于对比缓存。

(1)强制缓存

强制缓存,是在有缓存且有效的情况下,可以直接使用缓存,不需要请求服务器,判断缓存是否有效主要根据请求响应中的header中Expires/Cache-Control字段。Cache-Control优于Expires。
1、Expires
Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。是Http1.0的东西,基本可忽略。

Expires
2、Cache-Control
Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。
private: 客户端可以缓存
public: 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)
no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。
Cache-Control

(2)对比缓存

对比缓存表示需要与服务器对比决定缓存是否有效,第一次请求服务器返回缓存标识(header中的Last-Modified/Etag字段),第二次请求时header带上上次请求的缓存标识(字段If-Modified-Since/ If-None-Match),服务器如果判断客户端缓存有效,则返回304,否则返回200且将新的数据内容返回。
1、Last-Modified / If-Modified-Since

Last-Modified
If-Modified-Since
2、Etag / If-None-Match
优先级高于Last-Modified / If-Modified-Since
Etag
If-None-Match

(3)流程总结

第一次请求


第一次请求

再次请求


再次请求

二、OkHttp的缓存分析

(一)OkHttp启用缓存方式

启用缓存整体上需要配置两个地方,一是全局设置缓存目录及大小限制,二是构造Request时设置单个请求的缓存策略。
1、构造OkHttpClient时设置缓存目录:设置Cache

OkHttpClient.Builder builder = new OkHttpClient.Builder();
//指定缓存目录
File cacheDir = new File(Environment.getExternalStorageDirectory() + File.separator + "cache" + File.separator);
//设置缓存最大限制,官方推荐设置10M
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);
builder.cache(cache);
mClient = builder.build();

2、构造Request时设置缓存策略:设置CacheControl
如果没有对Request设置cacheControl默认会缓存处理。

 CacheControl cc = new CacheControl.Builder()
  //.noCache() //不使用缓存,但是会保存缓存数据
  // .noStore()  //不使用缓存,同时也不保存缓存数据
  //.onlyIfCached() //只使用缓存,如果本地没有缓存,会报504的错
  // .minFresh(10,TimeUnit.SECONDS) //10秒内缓存有效,之后无效,需要请求
  //.maxAge(10,TimeUnit.SECONDS) //接收有效期不大于10s的响应
   .maxStale(5,TimeUnit.SECONDS) //接收过期5秒的缓存
  build();
  Request request = new Request.Builder()
  .cacheControl(cc)
  .url("https://www.jianshu.com").build();

(二)CacheInterceptor 的总体缓存流程

OkHttp的缓存主要流程在拦截器CacheInterceptor中,下面通过分析源码了解相关逻辑。

public final class CacheInterceptor implements Interceptor {
 @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) {
      // 服务器返回304,说明缓存有效,将本地缓存和服务器响应结合后返回
      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 {
        // 没有返回304,说明缓存已经无效,清空缓存
        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.
        // 可以缓存,缓存服务器最新响应
        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;
  }
}

通过代码中注释可以了解基本的流程,可以总结为下面步骤:

  1. 如果开启了缓存,则根据请求Request寻找对应的候选缓存对象cacheCandidate,Cahce匹配缓存的逻辑后续分析;
  2. 创建缓存策略CacheStrategy,入参为当前时间、请对对象、缓存对象cacheCandidate。缓存策略是实现http缓存协议的主体,根据缓存对象的头部Expires/ETag等决定缓存的处理流程。缓存策略的出参主要是缓存网络请求networkRequest,缓存实际响应cacheResponse,是否为空决定了几种处理流程;
  3. 如果有缓存候选记录,但经过缓存策略处理,缓存实际响应为空,说明缓存已经失效,清空缓存的候选记录;
  4. 如果请求要求仅使用缓存,但缓存实际响应为空,则报504的错误;
  5. 如果请求要求仅使用缓存,且缓存实际响应不为空,则直接返回该响应;
  6. 到这一步,需要请求服务器,发送到请求分两种情况,一种是原始网络请求,例如没有匹配到缓存对象,或者当前网络请求是https,而缓存不是,又或者当前请求指定不使用缓存,第二种请求是发送缓存请求,带上If-None-Match/If-Modified-Since,由服务器判断本地缓存是否有效。如果网络请求抛错,则清空候选缓存;
  7. 如果服务器响应的code是304,说明本地缓存有效,本地缓存和最新网络响应合并后返回;否则清空本地候选的缓存记录;

(三)CacheStrategy源码分析

CacheStrategy表示缓存策略, 内部通过工厂类Factory构造不同的缓存策略,构造缓存策略入参包括当前时间、当前请求、缓存的记录,通过对缓存记录的头部信息Date、Expires、Last-Modified、ETag、Age(http缓存协议字段)判断缓存处理类型,最后通过缓存策略的networkRequest、cacheResponse两个字段来指定是直接使用缓存记录,还是使用网络请求,或者两者同时使用。

public final class CacheStrategy {
  /** The request to send on the network, or null if this call doesn't use the network. */
  public final @Nullable Request networkRequest;

  /** The cached response to return or validate; or null if this call doesn't use a cache. */
  public final @Nullable Response cacheResponse;
 public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.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);
          }
        }
      }
    }
    /**
     * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
     */
    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;
    }
 /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      //本地没有对应的缓存记录,则缓存策略是:网络请求是当前的网络请求,没有缓存记录
      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();
      // 如果请求指定不使用缓存,或者请求header已经带有缓存字段If-Modified-Since/If-None-Match
      // 则不使用该缓存,缓存策略是:网络请求是当前的网络请求,没有缓存记录
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

       // 如果缓存头部表明内容不变的,则缓存策略是:不需网络请求,有缓存记录 
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
      }

      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 {
        // 缓存记录header没有缓存字段,则缓存策略是:网络请求是当前的网络请求,没有缓存记录
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }

      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
      // 缓存记录header有Expires、Last-Modified、Etag等缓存字段,则构造缓存请求,
      // header带上if-None-Match/If-Modified-Since字段,该请求需要同时使用网络请求和缓存,
      // 缓存策略是:缓存网络请求,有缓存记录
      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

(四)Cache 核心方法分析

Cache类主要负责缓存文件管理:
● 内部实现InternalCache接口,对外提供调用方法;
● 内部通过DiskLruCache将cache写入文件系统;
● 通过requestCount,networkCount,hitCount三个指标跟踪缓存效率。

1、InternalCache 分析

OkHttp支持自定义缓存管理,自定义需要实现InternalCache 接口类,因为在缓存处理的流程调用的是该接口的方法,也可以使用自带的缓存管理Cache,Cache内部实现了InternalCache 接口,在接口的实例中调用Cache的方法,实现缓存管理。

 final InternalCache internalCache = new InternalCache() {
    // 通过Request对象寻找本地缓存记录
    @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);
    }
    // 移除Request对应的缓存记录
    @Override public void remove(Request request) throws IOException {
      Cache.this.remove(request);
    }
    // 服务器返回304,更新本地缓存记录,主要是更新headers,body部分不变
    @Override public void update(Response cached, Response network) {
      Cache.this.update(cached, network);
    }
   // 服务器返回304,更新缓存匹配指标,hitCount+1,
    @Override public void trackConditionalCacheHit() {
      Cache.this.trackConditionalCacheHit();
    }
   // 根据缓存策略更新缓存指标,requestCount++,如果需要网络请求则networkCount++,
   // 如果不需要则hitCount++
    @Override public void trackResponse(CacheStrategy cacheStrategy) {
      Cache.this.trackResponse(cacheStrategy);
    }
  };

从代码可以看到InternalCache的实现类最终会调用Cache的具体方法,下面分析 Cache的主要几个方法。

get方法
通过Request对象寻找本地缓存记录

@Nullable Response get(Request request) {
    String key = key(request.url());// 生成请求对应的key,对url进行MD5,再转16进制
    DiskLruCache.Snapshot snapshot;// 缓存快照
    Entry entry;
    try {
      snapshot = cache.get(key);// 根据key从DiskLruCache获取缓存快照 
      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 response = entry.response(snapshot);
   // 再做一遍检验,缓存记录与Request是否匹配:url/method/headers
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

  public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

put方法
缓存服务器响应到本地

@Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    // 不支持缓存的请求类型:POST、PATCH、PUT、DELETE、MOVE
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());// 移除缓存记录
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    if (!requestMethod.equals("GET")) {
       // 不缓存非GET请求,技术上可以做到HEAD请求和部分POST请求,但实现复杂且效益低
      // 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;
    }
    // 如果响应的Vary header包含*,则不缓存
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    // 构造缓存内容对象Entry 
    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;
    }
  }

update方法
服务器返回304,更新本地缓存记录,主要是更新headers,body部分不变

  void update(Response cached, Response network) {
    Entry entry = new Entry(network);
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        entry.writeTo(editor);
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

小结:
● OkHttp支持自定义缓存管理,需要实现接口类InternalCache ;
● 自带缓存管理类Cache,内部通过DiskLruCache对缓存文件进行管理;
● OkHttp以Request的url作为key来管理缓存内容;
● OkHttp的缓存只支持GET类型的请求;
● 如果响应的Vary header包含*,则不缓存;(Vary header的意义在于如何判断请求是否一样,如Vary: User-Agent 则表示即使请求相同,代理不同则认为是不同的请求,如用不同浏览器打开相同的请求,User-Agent 会不同,在缓存管理时需要认为是不同的请求。)

2、DiskLruCache分析

DiskLruCache的使用方式及原理这里不展开分析了,可以参考郭大神的经典文章:Android DiskLruCache完全解析,硬盘缓存的最佳方案

上一篇 下一篇

猜你喜欢

热点阅读