深入理解OkHttp源码(四)——缓存

OkHttp根据HTTP头部中的CacheControl进行缓存控制,而缓存的具体实现是使用的JakeWharton大神的DiskLruCache

Cache-Control

HTTP中的Cache-Control首部

HTTP头部中的Cache-Control首部可以指示对应请求该如何获取响应,比如应该直接使用缓存的响应还是应该从网络获取响应;可以指示响应该如何缓存,比如是否应该缓存下来还是设置一个过期时间等。Cache-Control首部的一些值既可以用于请求首部又可以用于响应首部。具体的值有no-cache、nostore、max-age、s-maxage、only-if-cached等。

OkHttp中的CacheControl类

CacheControl类是对HTTP的Cache-Control首部的描述。CacheControl没有公共的构造方法,内部通过一个Builder进行设置值,获取值可以通过CacheControl对象进行获取。Builder中具体有如下设置方法:
- noCache()
对应于“no-cache”,如果出现在响应首部,不是表示不允许对响应进行缓存,而是表示客户端需要与服务器进行再验证,进行一个额外的GET请求得到最新的响应;如果出现在请求首部,表示不适用缓存响应,即进行网络请求得到响应
- noStore()
对应于“no-store”,只能出现在响应首部,表明该响应不应该被缓存
- maxAge(int maxAge, TimeUnit timeUnit)
对应于“max-age”,设置缓存响应的最大存活时间。如果缓存响应达到了最大存活时间,那么将不会再使用而会进行网络请求
- maxStale(int maxStale,TimeUnit timeUnit)
对应于“max-stale”,缓存响应可以接受的最大过期时间,如果没有指定该参数,那么过期缓存响应将不会使用。
- minFresh(int minFresh,TimeUnit timeUnit)
对应于“min-fresh”,设置一个响应将会持续刷新的最小秒数。如果一个响应当minFresh过去后过期了,那么缓存响应不会再使用了,会进行网络请求。
onlyIfCached()
对应于“onlyIfCached”,用于请求首部,表明该请求只接受缓存中的响应。如果缓存中没有响应,那么返回一个状态码为504的响应。
CacheControl类中还有其他方法,这里就不一一介绍了。想了解的可以去API文档查看。
对于常用的缓存控制,CacheControl中提供了两个常量用于修饰请求,FORCE_CACHE指示只能使用缓存中的响应,哪怕该响应过期了;FORCE_NETWORK指示只能使用网络响应。

OkHttp的缓存实现

OkHttp的缓存实现主要包括一个接口和一个类。其中接口InternalCache是缓存接口,应用不应该实现该接口而应该直接使用Cache类。InternalCache中定义的方法有:根据请求查找响应、缓存响应、更新响应等。

Cache类

Cache中很多方法都是通过DiskLruCache实现的,对于DiskLruCache的使用可以参考下面两篇博客。
Android DiskLruCache完全解析,硬盘缓存的最佳方案
Android DiskLruCache 源码解析 硬盘缓存的绝佳方案
OkHttp在DiskLruCache的基础上修改了一些,将I/O操作都改成了使用Okio,其他基本不变。

Cache类将响应缓存到文件系统中以便可以重用和减少带宽。
为了测量缓存效率,Cache类跟踪三个数据:
- 请求数量:自缓存创建后,发起的该请求数量
- 网络命中数:需要请求进行网络请求的请求数量
- 缓存命中数:响应由缓存提供的请求的数量

有时一个请求可能会导致一个额外的缓存命中。如果缓存包含一个过期响应的副本,那么客户端会进行一个额外的GET请求。如果被修改了,那么服务器将会返回一个更新了的响应;如果过期响应仍然有效,那么将会返回”not modified”响应。这样的响应会同时增加网络命中数和缓存命中数。
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);
    }
  };

从代码中可以看出,InternalCache接口中的每个方法的实现都交给了外部类Cache,所以主要看Cache类中的各个方法,而Cache类的这些方法又主要交给了DiskLruCache来实现。
首先看一下缓存响应的put方法的实现:

private 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;
    }
    //不缓存非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;
    }

    //使用DiskLruCache进行缓存
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(urlToKey(response.request()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

从上面代码可以看到,首先是对请求的方法进行判断,概括起来就是一句话:只缓存请求方法为GET的响应。然后符合缓存的条件后,使用响应创建一个Entry对象,然后使用DiskLruCache写入缓存,最终返回一个CacheRequestImpl对象。cache是DiskLruCache的实例,调用edit方法传入响应的key值,而key值就是对请求调用urlToKey方法。下面是urlToKey的实现:

private static String urlToKey(Request request) {
    //MD5
    return Util.md5Hex(request.url().toString());
  }

从代码就可以看出是对请求的URL做MD5然后再得到MD5值的十六进制表示形式,这儿就不继续看了。
Entry实例就是要写入的缓存部分,主要看一下它的writeTo()方法,该方法执行具体的写入磁盘操作:

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

      //缓存HTTP响应行
      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');
      //是HTTPS请求,缓存握手、整数信息
      if (isHttps()) {
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName())
            .writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
        if (handshake.tlsVersion() != null) {
          sink.writeUtf8(handshake.tlsVersion().javaName())
              .writeByte('\n');
        }
      }
      sink.close();
    }

从上面的代码可以看到,写入缓存的不仅仅只是响应的头部信息,还包括请求的部分信息:URL、请求方法、请求头部。至此,我们看到对于一个请求和响应,缓存中的key值是请求的URL的MD5值,而value包括请求和响应部分。Entry的writeTo()方法只把请求的头部和响应的头部保存了,最关键的响应主体部分在哪里保存呢?答案在put方法的返回体CacheRequestImpl,下面是这个类的实现:

private final class CacheRequestImpl implements CacheRequest {
    private final DiskLruCache.Editor editor;
    private Sink cacheOut;
    private boolean done;
    private Sink body;

    public CacheRequestImpl(final DiskLruCache.Editor editor) {
      this.editor = editor;
      this.cacheOut = editor.newSink(ENTRY_BODY);
      this.body = new ForwardingSink(cacheOut) {
        @Override public void close() throws IOException {
          synchronized (Cache.this) {
            if (done) {
              return;
            }
            done = true;
            writeSuccessCount++;
          }
          super.close();
          editor.commit();
        }
      };
    }

    @Override public void abort() {
      synchronized (Cache.this) {
        if (done) {
          return;
        }
        done = true;
        writeAbortCount++;
      }
      Util.closeQuietly(cacheOut);
      try {
        editor.abort();
      } catch (IOException ignored) {
      }
    }

    @Override public Sink body() {
      return body;
    }
  }

看完put方法之后,再看一下get方法,get方法根据请求获取缓存响应:

Response get(Request request) {
    //得到Key值
    String key = urlToKey(request);
    //从DiskLruCache中得到缓存
    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;
  }

从代码中可以看到,首先是对请求的URL进行MD5计算得到key值,然后尝试根据key值从缓存中得到值,如果没有该值,说明缓存中没有该值,那么直接返回null,否则创建Entry对象,然后再从Entry中得到响应对象,如果请求和响应不匹配,那么也返回null,否则就返回响应对象。
下面是Entry的构造方法:

 public Entry(Source in) throws IOException {
      try {
        BufferedSource source = Okio.buffer(in);
        //读请求相关信息
        url = source.readUtf8LineStrict();
        requestMethod = source.readUtf8LineStrict();
        Headers.Builder varyHeadersBuilder = new Headers.Builder();
        int varyRequestHeaderLineCount = readInt(source);
        for (int i = 0; i < varyRequestHeaderLineCount; i++) {
          varyHeadersBuilder.addLenient(source.readUtf8LineStrict());
        }
        varyHeaders = varyHeadersBuilder.build();

        //读响应状态行
        StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
        protocol = statusLine.protocol;
        code = statusLine.code;
        message = statusLine.message;
        //读响应首部
        Headers.Builder responseHeadersBuilder = new Headers.Builder();
        int responseHeaderLineCount = readInt(source);
        for (int i = 0; i < responseHeaderLineCount; i++) {
          responseHeadersBuilder.addLenient(source.readUtf8LineStrict());
        }
        String sendRequestMillisString = responseHeadersBuilder.get(SENT_MILLIS);
        String receivedResponseMillisString = responseHeadersBuilder.get(RECEIVED_MILLIS);
        responseHeadersBuilder.removeAll(SENT_MILLIS);
        responseHeadersBuilder.removeAll(RECEIVED_MILLIS);
        sentRequestMillis = sendRequestMillisString != null
            ? Long.parseLong(sendRequestMillisString)
            : 0L;
        receivedResponseMillis = receivedResponseMillisString != null
            ? Long.parseLong(receivedResponseMillisString)
            : 0L;
        responseHeaders = responseHeadersBuilder.build();

        //是HTTPS协议,读握手、证书信息
        if (isHttps()) {
          String blank = source.readUtf8LineStrict();
          if (blank.length() > 0) {
            throw new IOException("expected \"\" but was \"" + blank + "\"");
          }
          String cipherSuiteString = source.readUtf8LineStrict();
          CipherSuite cipherSuite = CipherSuite.forJavaName(cipherSuiteString);
          List<Certificate> peerCertificates = readCertificateList(source);
          List<Certificate> localCertificates = readCertificateList(source);
          TlsVersion tlsVersion = !source.exhausted()
              ? TlsVersion.forJavaName(source.readUtf8LineStrict())
              : null;
          handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates);
        } else {
          handshake = null;
        }
      } finally {
        in.close();
      }
    }

在put方法中我们知道了缓存中保存了请求的信息和响应的信息,这个构造方法主要用于从缓存中解析出各个字段。具体字段如下:

 private final String url;
    private final Headers varyHeaders;
    private final String requestMethod;
    private final Protocol protocol;
    private final int code;
    private final String message;
    private final Headers responseHeaders;
    private final Handshake handshake;
    private final long sentRequestMillis;
    private final long receivedResponseMillis;

举个HTTP的例子,一个Entry类似于这样:

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

获得了包含首部信息的Entry之后,再调用response方法得到正在的响应,下面是response()方法的实现:

public Response response(DiskLruCache.Snapshot snapshot) {
      String contentType = responseHeaders.get("Content-Type");
      String contentLength = responseHeaders.get("Content-Length");
      Request cacheRequest = new Request.Builder()
          .url(url)
          .method(requestMethod, null)
          .headers(varyHeaders)
          .build();
      return new Response.Builder()
          .request(cacheRequest)
          .protocol(protocol)
          .code(code)
          .message(message)
          .headers(responseHeaders)
          .body(new CacheResponseBody(snapshot, contentType, contentLength))
          .handshake(handshake)
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(receivedResponseMillis)
          .build();
    }

从代码中可以看到,首相根据请求头信息创建出缓存的请求,再创建出响应,响应的首部 信息保存在Entry中,而主体部分是在传入的Snapshot中,主体是创建了一个CacheResponseBody对象。CacheResponseBody继承自ResponseBody类并且使用传入的Snapshot获得put中保存的响应主体部分。
再看一下删除缓存的remove方法的实现:

 private void remove(Request request) throws IOException {
    cache.remove(urlToKey(request));
  }

可以看到首先对请求的URL做MD5得到key值,然后在DiskLruCache中删除。
Cache中还有其他一些方法,比如update()、close()等方法就不再介绍了。

OkHttp的缓存使用

如果需要使用缓存时,那么首先需要做的是在创建OkHttpClient时指定配置Cache类,如下:

 OkHttpClient client=new OkHttpClient.Builder().cache(new Cache(new File("cache"),24*1024*1024)).build();

        Request request=new Request.Builder().url("http://www.baidu.com").build();

        Call call=client.newCall(request);

        try {
            Response response=call.execute();
            response.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

可以看到,通过Builder的cache()方法传入一个Cache类,主要有两个参数,一个是目录,一个是缓存目录的大小。当执行上段代码后,就会生成一个cache目录,并且下面有一个journal文件,文件内容如下:

libcore.io.DiskLruCache
1
201105
2

Cache设置及初始化

Cache的设置均在OkHttpClient的Builder中设置,有两个方法可以设置,分别是setInternalCache()和cache()方法,如下:

/** Sets the response cache to be used to read and write cached responses. */
    void setInternalCache(InternalCache internalCache) {
      this.internalCache = internalCache;
      this.cache = null;
    }

    public Builder cache(Cache cache) {
      this.cache = cache;
      this.internalCache = null;
      return this;
    }

从代码中可以看出,这两个方法会互相消除彼此。在之前讲到的InternalCache类,该类是一个接口,文档中说应用不应该实现该类,所以这儿,我也明白为什么OkHttpClient为什么还提供这样一个接口。
当设置好Cache后,我们再来看下Cache的构造方法:

public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

可以看到暴露对外的构造方法只有两个参数,一个目录,一个最大尺寸,而其内部使用的DiskLruCache的create静态工厂方法。这里面FileSystem.SYSTEM是FileSystem接口的一个实现类,该类的各个方法使用Okio对文件I/O进行封装。
DiskLruCache的create()方法中传入的目录将会是缓存的父目录,其中ENTRY_COUNT表示每一个缓存实体中的值的个数,这儿是2。(第一个是请求头部和响应头部,第二个是响应主体部分)至此,Cache和其底层的DiskLruCache创建成功了。

CacheInterceptor

CacheInterceptor的初始化是在getResponseWithInterceptorChain方法中,使用OkHttpClient的internalCache()方法得到的InternalCache作为参数。下面首先看一个OkHttpClient的internalCache()方法:

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

从代码可以看出,如果cache不为null,那么就返回cache的internalCache字段,否则就返回internalCache。前面介绍Cache类时说过,其内部有一个internalCache字段,实现了InternalCache接口,而各种方法又是通过Cache中的方法实现的。接下来看CacheInterceptor的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.
    }

    // 只要缓存响应,但是缓存响应不存在,返回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(EMPTY_BODY)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // 不使用网络,直接返回缓存响应
    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 (cacheResponse != null) {
      //需要更新
      if (validate(cacheResponse, networkResponse)) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .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 (HttpHeaders.hasBody(response)) {
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }

    return response;
  }

从代码中可以看出,首先如果cache不为null,就根据请求先去缓存中查找响应,然后创建CacheStrategy对象。
CacheStrategy对象对一个请求和一个缓存响应作分析,指出应该使用网络响应还是缓存响应,又或者两者都要。
该类中有两个字段:
- networkRequest:发送到网络上的请求,如果为null表明该请求不需要使用网络
- cacheResponse:返回的或重验证的缓存响应,如果为null表明该请求不使用缓存

在intercept方法可以看到根据得到的候选响应、当前时间和请求创建一个工厂,然后再得到一个CacheStrategy。首先看该工厂的构造方法:

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

从代码中可以看出,如果候选的缓存响应不为null,那么将响应首部中有关缓存的首部的值得到,主要有Date、Expires、Last-Modified、ETag和Age首部。
其中Date表明响应报文是何时创建的,Expires表示该响应的绝对过期时间,Last-Modified表示最近一次修改的时间。
Factory的get方法如下:

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

从代码中可以看出首先调用getCandidate()得到候选的CacheStrategy对象,然后如果得到的缓存策略表明需要使用网络,但是请求中指定响应只能从缓存中得到,那么返回一个networkRequest和cacheResonse均为null的CacheStrategy。
下面主要看一下getCandidate方法,该方法返回的策略是基于请求可以使用网络的假设之上的,所以这也就解释了get()方法中为什么要对使用网络但是请求却指定缓存响应的情况做区分。

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

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

从代码可以得到几种情况:
- cacheResponse==null,表明缓存中没有该响应,那么直接进行网络请求
- 如果请求是HTTPS并且缓存响应中没有握手,那么需要重新进行网络请求
- 如果响应不应该被存储,那么需要重新进行网络请求
- 如果请求中指定不使用缓存响应,那么需要进行网络请求。
- 接下来比较缓存响应检查是否有条件请求的首部,如果有,就进行额外的请求

这时候,再来看intercept方法,得到了CachaStartegy后得到其两个字段,然后如果cache不为null,调用trackResponse()方法,下面是Cache类的trackResponse()方法:

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

可以看到该方法主要就是对Cache中的三个记录进行赋值,从这儿我们可以得出结论requestCount>=networkCount+hitCount,当networkRequest和cacheResponse均为null的时候,这个时候的响应既不是从网络得到也不是从缓存得到。
下面分为几种情况:
- networkRequest==null&&cacheResponse\==null:这种情况就是Factory的get方法中那个特例,表示只能从缓存得到响应,但是缓存中没有该响应时的情况。返回一个状态码为504的响应。
- networkRequest==null:表明不需要使用网络进行请求,那么使用缓存响应。但是返回的响应确是没有响应主体的
- 接下来是需要进行网络请求的请求,那么就调用chain.proceed得到网络响应

接下来,如果cacheResponse不为null的话,表明即有网络请求又有缓存响应,表示该请求是一次额外的请求——重验证。那么需要判断缓存响应是不是最新的,如果不是的,那么需要以网络响应合并缓存响应,然后调用Cache的trackConditionalCacheHit对hitCount进行+1操作和更新缓存中的响应。
如果cacheResponse为null,表明该网络请求不是一次额外的请求;或者cacheResponse没有过期,那么也重新创建一个响应,如果该响应有主体部分,那么就行缓存操作,最后返回。

再看Response类

在分析完上面的CacheInterceptor之后,对Response可能会有些迷惑,一会儿是cacheResponse()方法,一会又是networkResponse,并且后面存储的响应都是没有主体的,那么真实的响应主体又在何处呢?
Response类有如下几个类型的字段:

private final Request request;
  private final Protocol protocol;
  private final int code;
  private final String message;
  private final Handshake handshake;
  private final Headers headers;
  private final ResponseBody body;
  private final Response networkResponse;
  private final Response cacheResponse;
  private final Response priorResponse;
  private final long sentRequestAtMillis;
  private final long receivedResponseAtMillis;

  private volatile CacheControl cacheControl; // Lazily initialized.

其中networkResponse表示从网络上得到的响应。如果该响应没有使用网络,那么将会为null。networkResponse的主体不应该被读;
cacheResponse表示从缓存上得到的响应。如果该响应没有使用缓存,那么将会为null。对于额外的GET请求,networkResponse和cacheResponse可能均不为null。cacheResponse的主体不应该被读。
priorResponse表示触发该响应重定向或认证的前一个响应。如果该响应不是由自动重试触发的,那么将会为null。priorResponse的主体不应该被读,因为它已经被重定向客户端消费了。
关于Response有一点需要铭记,该类的实例不是一成不变的,响应主体部分只能被消费一次然后关闭,其他参数是不变的。
那么为什么Response的主体部分只能被消费一次呢?
这是因为ResponseBody的底层是用Okio实现的,而Okio的Source只能被读取一次,因为读完之后,Buffer底层的Segment关于之前数据的信息(pos和limit)就丢失了,并且在读完一次之后就将Source关闭了,所以只能读一次。关于Okio的可以参考拆轮子系列:拆 Okio

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值