Okhttp 之HTTP 缓存实现

以前学习 HTTP 的时候呢,都是从书本中学习概念,但是从来没有在服务器端和客户端去实践过。 作为移动开发人员呢,我觉得还是有必要了解HTTP在客户端的实现。这篇文章讲述的是 Okhttp 在缓存方面的实现,这需要你对 HTTP 缓存有个概要的认识,然后我们结合 Okhttp 源码来看看如何实现客户端的 HTTP 缓存,方便以后与服务器开发人员交(装)流(B)。

设置缓存

    sOkHttpClient = new OkHttpClient.Builder()
            .cache(new Cache(getCacheDir(), 10 * 1024 * 1024))
            .build();

很简单,只需要在创建 Cache 的时候指定缓存的目录和大小即可。这个缓存将会在 CacheInterceptor 中用到,CacheInterceptor 是在 RealCallgetResponseWithInterceptorChain() 方法中加入的

RealCall.java

  Response getResponseWithInterceptorChain() throws IOException {
    // ...

    interceptors.add(new CacheInterceptor(client.internalCache()));

    // ...
}

从缓存中获取响应和把响应写入缓存,这两个过程都是在 CacheInterceptor.intercept() 中执行的。

缓存未命中

缓存未命中是针对一个请求,没有从缓存中获取到响应。下面就分析这个情况。

CacheInterceptor.java

    public Response intercept(Chain chain) throws IOException {
        // 从缓存中获取响应
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;

        long now = System.currentTimeMillis();
        // 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);
        }

        // ...

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

        // ...

        Response response = networkResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                // 去掉 body ,留下 start line 和 headers
                .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;
    }

3行,从缓存中获取响应,由于分析的缓存未命中的情况,此时为 null

911行,是从缓存策略中获取请求和响应,这里涉及到的是缓存的有效性和服务器再验证过程。由于分析的缓存未命中的情况,因此,networkRequest 也就是原请求,cacheResponsenull

15 行,使用 Cache 来跟踪请求的次数,网络请求次数或缓存命中次数。源码如下

Cache.java

    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.
            hitCount++;
        }
    }

22 行表明缓存策略的缓存响应无效,就执行网络请求,这个请求过程并不在本文的分析范围内,因此不深究。

32 行,重新构造 Response 对象,加入 cacheResponsenetworkResponse 属性。stripBody() 方法,其实就是把响应中的 body去掉,保留 start lineheaders

Cache.java

    private static Response stripBody(Response response) {
        return response != null && response.body() != null
            ? response.newBuilder().body(null).build()
            : response;
    }

由于缓存未命中,因此 cacheResponse() 保存的值为 null

40 行,可以看到有两个条件,一个是判断响应中是否有 body,一个是判断是否能进行缓存。一旦成立,就代表我们需要更新缓存。

首先看看 HttpHeaders.hasBody(response) 如何判断是否有 body 的。

HttpHeaders.java

    /** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */
    public static boolean hasBody(Response response) {
        // HEAD requests never yield a body regardless of the response headers.
        if (response.request().method().equals("HEAD")) {
            return false;
        }

        int responseCode = response.code();
        if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
                && responseCode != HTTP_NO_CONTENT
                && responseCode != HTTP_NOT_MODIFIED) {
            return true;
        }

        // If the Content-Length or Transfer-Encoding headers disagree with the response code, the
        // response is malformed. For best compatibility, we honor the headers.
        if (contentLength(response) != -1
                || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
            return true;
        }

        return false;
    }

HTTP 的响应在哪些情况下是没有 body的?
1. HEAD 请求是没有 body,但是可以有 Content-Length 属性。
2. 响应码为 1XX, 204, 304 的响应是没有 body的。 100~199 的代表的是信息性状态码,用于客户端和服务器之间的信息交换,例如确认服务器是否可以处理客户端的 body204代表 No Content,这个主要用于浏览器,向服务器提交数据,但是不需要浏览器展示数据更新后的页面。304 代表 Not Modified,这个用来对服务器进行有条件的请求,用来验证缓存资源的有效性,如果缓存资源有效,服务器会返回 304,并不带 body

但是在 HttpHeaders.hasBody() 的方法中,它还处理了一种兼容的情况,对于那些不能带有 body 的状态码, 如果响应首部Content-Length 值不为 -1Transfer-Encodingchunked,也可以带有 body。(可能为兼容 HTTP/1.1 之前的协议?)

再来看看 CacheStrategy.isCacheable() 方法如何判断能够缓存的。

CacheStrategy.java

    /** Returns true if {@code response} can be stored to later serve another request. */
    public static boolean isCacheable(Response response, Request request) {
        // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
        // This implementation doesn't support caching partial content.
        switch (response.code()) {
            // https://tools.ietf.org/html/rfc7231#section-6.1
            case HTTP_OK: // 200
            case HTTP_NOT_AUTHORITATIVE: // 203
            case HTTP_NO_CONTENT: // 204
            case HTTP_MULT_CHOICE: // 300 
            case HTTP_MOVED_PERM:// 301
            case HTTP_NOT_FOUND: // 404
            case HTTP_BAD_METHOD: // 405
            case HTTP_GONE: // 410
            case HTTP_REQ_TOO_LONG: // 414
            case HTTP_NOT_IMPLEMENTED: // 501
            case StatusLine.HTTP_PERM_REDIRECT: // 308
                // These codes can be cached unless headers forbid it.
                break;

            case HTTP_MOVED_TEMP: // 302
            case StatusLine.HTTP_TEMP_REDIRECT: // 307
                // These codes can only be cached with the right response headers.
                // http://tools.ietf.org/html/rfc7234#section-3
                // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
                if (response.header("Expires") != null
                        || response.cacheControl().maxAgeSeconds() != -1
                        || response.cacheControl().isPublic()
                        || response.cacheControl().isPrivate()) {
                    break;
                }
                // Fall-through.

            default:
                // All other codes cannot be cached.
                return false;
        }

        // A 'no-store' directive on request or response prevents the response from being cached.
        return !response.cacheControl().noStore() && !request.cacheControl().noStore();
    }

从实现可以看出,除去这13种响应码,其它的响应码都是不可以进行缓存的。而这13种缓存,最终是由请求报文的响应报文的 Cache-Control:no-store 共同决定的,也就是 CacheStrategy.isCacheable() 最后一个 return 语句的效果。

  1. https://tools.ietf.org/html/rfc7231#section-6.1http://tools.ietf.org/html/rfc7234#section-3 给出能够响应的状态码,但是好像并没有给全的样子。
  2. CacheStrategy.isCacheable() 并不能缓存范围请求(Range)的响应,也就状态码为 206 的响应。
  3. 忽略共享缓存属性 Cache-Control: s-maxage,因为应用的缓存是私有缓存。

现在回到 CacheInterceptor.intercept() 的第 43 行,到这里代表了响应有 body,而且能够进行缓存,于是乎,把响应在缓存中添加一个副本。

Cache.java

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

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

首先 HttpMethod.invalidatesCache()4 行判断哪些请求方法的缓存是无效的。

HttpMethod.java

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

这些方法都是对服务器数据进行操作的方法,因此不需要缓存响应。如果已经缓存过,就需要删除缓存。 否则,后续的这些对服务器数据进行操作的方法可能就不会传达到服务器,可能经过缓存就返回了。

然后 HttpMethod.invalidatesCache()12 行,也并没有缓存非 GET 的响应,从注释中可以看出,从技术角度看,是可以缓存 HEAD 和 某些 POST 请求的,然而由于过于复杂,缓存这些响应弊大于利,因此 Okhttp 并不会对这些请求进行缓存。

然后 HttpMethod.invalidatesCache()19 行,如果响应头中带有 Vary 头部,也不进行缓存。 Vary 是协商首部,服务器会根据这个首部列出的其它首部列表决定响应。因此这个带有 Vary 首部的响应应该是不“稳定的”,没必要缓存(一脸懵B)。

HttpMethod.invalidatesCache() 最后把这个响应写入到 OkhttpDiskLruCache 缓存中,但是不要以为是直接读取响应的 body ,然后写入缓存,它只是写入了 start lineheader 的信息

Cache.Entry

    public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url)
          .writeByte('\n');
      sink.writeUtf8(requestMethod)
          .writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(varyHeaders.value(i))
            .writeByte('\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString())
          .writeByte('\n');
      sink.writeDecimalLong(responseHeaders.size() + 2)
          .writeByte('\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(responseHeaders.value(i))
            .writeByte('\n');
      }
      sink.writeUtf8(SENT_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(sentRequestMillis)
          .writeByte('\n');
      sink.writeUtf8(RECEIVED_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(receivedResponseMillis)
          .writeByte('\n');

      if (isHttps()) {
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName())
            .writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
      }
      sink.close();
    }

从实现可以看出,它只是把 url, method, Vary 首部,start line 等等一些属性写入 DiskLruCache,然而并不包括 body。那么 body 在什么写入的呢? 当然是用户开始读取的时候写入缓存。

注意 HttpMethod.invalidatesCache()31行,它返回了一个 CacheRequest 接口的实现类,它提供了向缓存写入的接口。

现在回到 CacheInterceptor.intercept() 的第 45 行,cacheWritingResponse() 它会创建一个新的 Source,当用户开始读取数据的时候,就会向缓存中写入数据。

CacheInterceptor.java

    private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
            throws IOException {
        // Some apps return a null body; for compatibility we treat that like a null cache request.
        if (cacheRequest == null) return response;
        Sink cacheBodyUnbuffered = cacheRequest.body();
        if (cacheBodyUnbuffered == null) return response;

        final BufferedSource source = response.body().source();
        final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

        Source cacheWritingSource = new Source() {
            boolean cacheRequestClosed;

            @Override public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead;
                try {
                    bytesRead = source.read(sink, byteCount);
                } catch (IOException e) {
                    if (!cacheRequestClosed) {
                        cacheRequestClosed = true;
                        cacheRequest.abort(); // Failed to write a complete cache response.
                    }
                    throw e;
                }

                if (bytesRead == -1) {
                    if (!cacheRequestClosed) {
                        cacheRequestClosed = true;
                        cacheBody.close(); // The cache response is complete!
                    }
                    return -1;
                }

                sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
                cacheBody.emitCompleteSegments();
                return bytesRead;
            }

            @Override public Timeout timeout() {
                return source.timeout();
            }

            @Override public void close() throws IOException {
                if (!cacheRequestClosed
                        && !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
                    cacheRequestClosed = true;
                    cacheRequest.abort();
                }
                source.close();
            }
        };

        String contentType = response.header("Content-Type");
        long contentLength = response.body().contentLength();
        return response.newBuilder()
                .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
                .build();
    }

在前面的文章 Okhttp 之 okio 剖析过 okio,如果有这个基础,就可以看懂这段代码,这里我就不再解析了。 总之这段代码的作用就是当用户开始读取数据的时候,就会向缓存中写入数据。

缓存命中

缓存命中的情况比未命中的情况要复杂的多,因为这涉及到缓存验证问题,而缓存验证问题是由 CacheStrategy 决定的,因此重点就是这个 CacheStragy 类。先看下如何创建 CacheStratety 对象的。

CacheInterceptor.java

    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;

        // ...

        return response;
    }

CacheInterceptor.intercept()8 行, CacheStrategyFactory()方法

CacheStrategy.java

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

CacheStrategy.Factor() 方法其实就是解析一些用于验证缓存有效性的 headerDate, Expires, Last-Modified, ETag, Age

现在再看看 CacheStrategy.get() 方法,它是用来获取 CacheStrategy 实例的。

CacheStrategy.java

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

getCandidate() 是用来验证缓存的有效性,要看懂这个方法,需要对 HTTP 缓存 有个全面的认识,这里来过一遍

CacheStrategy.java

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

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

3 行,这就是之前分析的缓存未命中的情况。

8 行,会对 HTTPS 协议的请求,如果没有 handskake 过程,就需要重新进行网络请求,也就是 networkRequest=request,而 cacheResponse=null

15 行,排除了不能缓存的一些请求方法,这个 isCacheable() 方法在前面分析过。如果不能缓存,就只能执行网络请求,也就是 networkRequest=request,而 cacheResponse=null

20 行,如果请求报文有 Cache-Control:no-cache属性,或者有条件首部(If-Modified-SinceIf-None-Match),就不能从缓存中直接获取,还是需要重新从网络获取。

25 行,如果响应报文有 Cache-Control:immutable属性,就直接从缓存中获取。

2556 行,通过计算缓存资源的使用期和新鲜度,来估算缓存有效性,这个算法有点讲究,在 <<HTTP权威指南>> 的一书的 7.11 节,有对这个算法作详细的解释,这里就不去深究缓存算法了,有点烧脑。

6270 行,分别用缓存响应的 ETag, Last-Modified, Dateheader属性进行条件请求验证,如果没有这些请求头,就直接做网络请求。

7576 行,为 OkhttpClient 设置这些条件请求头。

7880 行,为请求报文设置设置这些请求条件。

最后根据以上的种种情况,返回相应的 CacheStrategy 对象,也就是返回相应的策略,到底是直接用缓存,还是要直接用网络请求,还是要进行条件性请求验证。

现在明白了缓存的策略,那么我们再回头分析 CacheInterceptor.intercept() 的的缓存命中情况的代码

CacheInterceptor.java

    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,而缓存策略的缓存响应为null,证明缓存不可用,关闭它。
        if (cacheCandidate != null && cacheResponse == null) {
            closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
        }

        // 如果缓存策略禁止网络请求,同时缓存无效,直接返回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();
        }

        // 如果不需要进行网络请求,同时响应不为null,就直接返回缓存中的副本
        // 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) {
            if (networkResponse.code() == HTTP_NOT_MODIFIED) { // 服务器返回304 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 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;
    }

当我们获取到了 CacheStrategy 缓存策略后,就可以获取 networkRequestcacheResponse,也就是第 910 行所做的。那么组合 networkRequestcacheResponse 就有四种情况了。
1. networkRequest = nullcacheResponse = null,无效请求(2131行)。
2. networkRequest = nullcacheResponse != null,缓存有效,直接返回缓存,(3438 行)。
3. networkRequest != nullcacheResponse = null,就需要直接网络请求(42行),然后重构响应,并更新缓存(7291行)。
4. networkRequest != nullcacheResponse != null,就需要直接网络请求(42行),然后根据服务器响应状态码,更新缓存或者重构缓存(5191行)。

结束

在实际的应用中,客户端缓存实际上很大部分取决于服务器的实现,然而很多时候,我们并没有控制服务器的能力,因而客户端的缓存很可能并没有用,比如你访问百度,虽然页面总是一样,但是我们手机端是不会缓存的,因为在响应报文中没有设置用于缓存的 header。 而一旦,我们能够与服务器开发人员交互,那么掌握客户端缓存的实现,就有助于我们与服务器开发人员更好的交(装)流(B)。

发布了44 篇原创文章 · 获赞 30 · 访问量 400万+

猜你喜欢

转载自blog.csdn.net/zwlove5280/article/details/79916662