okhttp源码阅读(缓存)

96
史提芬陈
2018.09.28 21:01* 字数 920

上一篇分析了okhttp的执行流程,这篇来分析下okhttp的缓存策略,
首先了解一下HTTP的缓存策略。

1.Cache-Control HTTP缓存操作机制

通过指定Cache-Control的指令,就能操作缓存的工作机制。

1.1 no-cache指令

使用no-cache指令是为了防止读取缓存中过期的资源,可以使用缓存但必须向原始服务器认证。

1.2 no-store指令

no-store指令表示不使用缓存。

1.3 only-if-cached指令

only-if-cached指令表明客户端只接受已缓存的响应,若缓存不可用则返回状态码504。

1.4 max-age指令(单位:秒)

max-age指令指定从请求的时间开始,允许获取的响应被重用的最长时间(单位:秒),在HTTP/1.1下如果同时存在Expires首部字段时,会优先处理max-age指令,在HTTP/1.0下则相反。

1.5 min-fresh指令(单位:秒)

min-fresh表示客户端可以接受当前的age加上min-fresh设定的时间之和内的响应。

1.6 max-stale指令(单位:秒)

表明客户端愿意接收一个已经过期的资源,但过期时间必须小于max-stale 值的缓存对象。

2.条件请求

当请求首部字段包括If-xxx这种样式时,都可称为条件请求,服务器接收到附带条件请求后,只有判断指定条件为真时,才会执行请求。

2.1 ETag

服务器返回的资源标识,当资源更新时,ETag值也需要更新。

2.2 If-Match

服务器会比对If-Match的字段值和资源的ETag值,仅当两者一致时,才会执行请求。反之返回状态码412的响应。

2.3 If-None-Match

服务器会比对If-None-Match的字段值和资源的ETag值,仅当两者不一致时,才会执行请求。与If-Match的作用相反。

304 Not Modified

该状态码表示客户端发送条件请求时,服务端返回304 Not Modified代表这个资源未被修改,可以使用缓存的资源。

OkHttp缓存分析

OkHttp的缓存工作是在缓存拦截器(CacheInterceptor)里面执行的。
来看一下CacheInterceptor这个类

CacheInterceptor
/** Serves requests from the cache and writes responses to the cache. */
public final class CacheInterceptor implements Interceptor {
  final InternalCache cache;

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

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

    //如果获取的本地缓存不为空,且验证过后的响应缓存等于null 说明这个缓存已经不可用了,可能是过期了或者其他原因
    if (cacheCandidate != null && cacheResponse == null) {
      //把缓存清除掉
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //如果两个为空直接返回504 ,这个代表客户端设置了不请求网络只允许拿本地缓存作为响应结果,并且不存在本地缓存所以返回504
    // If we're forbidden from using the network and the cache is insufficient, fail.
    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 {
        //缓存已改变,清除旧的缓存
        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;
  }

  //...省略
}

CacheInterceptor的关键方法是intercept,intercept方法逻辑都有注释说明,看代码即可。在intercept方法的第一句是通过Request获取本地缓存

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

来看一下CacheInterceptor是如何获取本地缓存的,首先分析Cache这个类

Cache
public final class Cache implements Closeable, Flushable {
  
  //缓存管理类,LRU算法
  final DiskLruCache cache;
  
  //...省略
  
  //网络请求数量
  private int networkCount;
  //缓存命中数量
  private int hitCount;
  //请求数量
  private int requestCount;

  //...省略
  
  //获取存储缓存的key
  public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }
  
  //获取缓存
  @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;
  }

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

  void remove(Request request) throws IOException {
    cache.remove(key(request.url()));
  }

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

Cache的代码主要是一些磁盘管理的代码,可以看到OkHttp最终使用DiskLruCache来管理缓存,DiskLruCache是个开源库,关于DiskLruCache的解析这里不展开。

在上文分析CacheInterceptorintercept方法中可以看到,逻辑的处理依赖于生成的缓存策略CacheStrategy

//生成缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
//网络请求
Request networkRequest = strategy.networkRequest;
//本地响应缓存
Response cacheResponse = strategy.cacheResponse;

缓存策略根据工厂类通过实际请求本地缓存获取对应的缓存策略,来看一下一个缓存策略是如何生成的

/**
 * 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() {
  // No cached response.
  //本地不存在缓存
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  //https 链接并且握手失败
  // Drop the cached response if it's missing a required handshake.
  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);
  }

  //http header 的这个字段"immutable"代表响应结果是不可变的,
  // 所以如果这个字段为true,代表如果存在本地缓存则直接使用不需要在发起网络请求
  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());
  }

  //过期后的 maxStaleMillis 秒内缓存可以继续使用
  long maxStaleMillis = 0;
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  //ageMillis + minFreshMillis < freshMillis + maxStaleMillis 缓存还能使用
  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;
    //缓存的有效时间超过24小时
    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) {
    //If-None-Match,它和ETags一起判断资源是否被修改
    conditionName = "If-None-Match";
    conditionValue = etag;
  } else if (lastModified != null) {
    //Last-Modified和HTTP-IF-MODIFIED-SINCE只判断资源的最后修改时间
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
  } else if (servedDate != null) {
    //存活时间和HTTP-IF-MODIFIED-SINCE
    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);
}

关于一个缓存策略是如何生成的看代码注释就好了,这里就不再详细说明。

总结:要理解OkHttp的缓存策略需要理解HTTP的缓存机制,另外可以看到OkHttp的缓存机制只有硬盘缓存并没有内存缓存这也是它的缓存机制不太完美的地方。

okhttp
Web note ad 1