OKHttp的缓存策略(三)

706 阅读5分钟

OKhttp的缓存是通过CacheInterceptor这个拦截器实现的,它的实现如下:

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;//先从缓存中取出Request对应的Response,可能为null

    long now = System.currentTimeMillis();
    //通过Request和Response构造CacheStrategy
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    //缓存监控
    if (cache != null) {
      cache.trackResponse(strategy);
    }
    //cacheCandidate无效,关闭它
    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.
    //networkReqeust和cacheResponse都为null表示禁止使用网络且缓存不够用,这是返回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))//配置Response的cacheResponse,但并不需要持有body所以这里剔除掉body
          .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;//返回cacheResponse的内容
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    //网络请求的Response
    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;//返回结果
  }

  //将Response的body剔除  
  private static Response stripBody(Response response) {
    return response != null && response.body() != null
        ? response.newBuilder().body(null).build()
        : response;
  }
  ……
}

CacheInterceptor内部持有一个InternalCache,它负责缓存的存取,而CacheStrategy用来控制缓存的存取,决定什么时候用缓存,什么时候使用网络进行请求。它是通过其内部的networkRequest和cacheResponse决定的,当networkRequest的值为null表示缓存有效不进行网络请求,而当cacheResponse为null是表示缓存失效或者未命中,需要进行网络请求。当它们都不为null,则根据响应结果来判断缓存是否失效。

CacehStrategy通过Request和候选的缓存进行构造的,我们看看它的内部是如何实现的

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

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 static class Factory {
        final long nowMillis;
        final Request request;
        final Response cacheResponse;

        /** The server's time when the cached response was served, if known. */
        private Date servedDate;
        private String servedDateString;

        /** The last modified date of the cached response, if known. */
        private Date lastModified;
        private String lastModifiedString;

        /**
        * The expiration date of the cached response, if known. If both this field and the max age are
        * set, the max age is preferred.
        */
        private Date expires;

        /**
        * Extension header set by OkHttp specifying the timestamp when the cached HTTP request was
        * first initiated.
        */
        private long sentRequestMillis;

        /**
        * Extension header set by OkHttp specifying the timestamp when the cached HTTP response was
        * first received.
        */
        private long receivedResponseMillis;

        /** Etag of the cached response. */
        private String etag;

        /** Age of the cached response. */
        private int ageSeconds = -1;

        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;//得到资源对应的ETag
                } 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() {
            // No cached response.
            //没有命中的缓存,使用网络进行请求
            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
            CacheControl requestCaching = request.cacheControl();
            //如果不进行缓存则直接进行网络请求
            if (requestCaching.noCache() || hasConditions(request)) {
                return new CacheStrategy(request, null);
            }

            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;
            CacheControl responseCaching = cacheResponse.cacheControl();
            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) {//etag不为null
                conditionName = "If-None-Match";
                conditionValue = etag;//将If-None-Match配合etag进行请求,服务端可以基于ETag判断资源是否匹配。
            } else if (lastModified != null) {//在请求头部添加If-Modified-Since结合lastModifiedString判断资源是否被修改,如果未修改返回304
                conditionName = "If-Modified-Since";
                conditionValue = lastModifiedString;
            } else if (servedDate != null) {//在请求头部添加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);
        }
    }
}

在getCandidate方法中实现了构造CacheStrategy实例的逻辑,总的来说:

一 使用网络进行请求的包括,即networkRequest != null :

  1. 缓存未命中
  2. https请求缺少handshake信息
  3. request指定的CacheControl不进行缓存
  4. 缓存的Response头部没有缓存相关的信息如ETag,If-None-Match,If-Modified-Since等

二 使用缓存请求的情况,即cacheResponse != null

  1. 缓存有效,未超过其失效时间

三 networkRequest != null 且 cacheResponse != null

  1. 缓存是否有效,要根据响应结果来判断

缓存的写入

在CacheInterceptor内部它对Response进行缓存的实现如下:

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类中实现的,它内部持有一个internalCache,它正是CacheInterceptor拦截器内部的缓存引用。这里我们先看cache put方法的实现

@Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();//获取requestMethod
    //判断是否为失效的缓存
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    //只对GET响应进行缓存
    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 entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //得到DiskLruCache.Editor 可以看到OkHttp缓存是通过DiskLruCache实现的
      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;
    }
}

关于DiskLruCache将在另外的篇章中介绍。