OKHTTP系列(六)--拦截器之CacheInterceptor缓存拦截器

我们知道为了节省流量和提高响应速度,Okhttp是有自己的一套缓存机制的,CacheInterceptor就是用来负责读取缓存以及更新缓存的。

一、CacheInterceptor拦截器intercept(Chain chain)方法

/** Serves requests from the cache and writes responses to the cache. */
//从缓存中服务请求并将响应写入缓存
public final class CacheInterceptor implements Interceptor {
    @Override public Response intercept(Chain chain) throws IOException {
//1. 读取候选缓存,具体如何读取的我们下面会讲。
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;

        long now = System.currentTimeMillis();
//2. 创建缓存策略,强制缓存、对比缓存等,关于缓存策略我们下面也会讲。
        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.
//3. 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码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.
//4. 根据策略,不使用网络,有缓存的直接返回。
        if (networkRequest == null) {
            return cacheResponse.newBuilder()
                    .cacheResponse(stripBody(cacheResponse))
                    .build();
        }

        Response networkResponse = null;
        try {
//5. 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。
            networkResponse = chain.proceed(networkRequest);
        } finally {
            // If we're crashing on I/O or otherwise, don't leak the cache body.
//如果发生IO异常,则释放掉缓存
            if (networkResponse == null && cacheCandidate != null) {
                closeQuietly(cacheCandidate.body());
            }
        }

        // If we have a cache response too, then we're doing a conditional get.
//6. 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。
        if (cacheResponse != null) {
            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()).
//在合并标头之后但在剥离Content-Encoding标头之前更新缓存(由initContentStream()执行)。
                cache.trackConditionalCacheHit();
                cache.update(cacheResponse, response);
                return response;
            } else {
                closeQuietly(cacheResponse.body());
            }
        }
//7. 读取网络结果。
        Response response = networkResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();
//8. 对数据进行缓存。
        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 {
//删除所提供的{@code request}的所有缓存条目。 当客户端使缓存失效时(例如在发出POST请求时),将调用此方法。
                    cache.remove(networkRequest);
                } catch (IOException ignored) {
                    // The cache cannot be written.
                }
            }
        }
//9. 返回网络读取的结果。
        return response;
    }
}

从上面的源码可以得出整个方法的流程:

  • 读取候选缓存,具体如何读取的我们下面会讲。
  • 创建缓存策略,强制缓存、对比缓存等,关于缓存策略我们下面也会讲。
  • 根据策略,不使用网络,又没有缓存的直接报错,并返回错误码504。
  • 根据策略,不使用网络,有缓存的直接返回。
  • 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。
  • 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。
  • 读取网络结果。
  • 对数据进行缓存。
  • 返回网络读取的结果。

二、HTTP与缓存相关的理论知识

在分析Okhttp的缓存机制之前,我们先来回顾一下HTTP与缓存相关的理论知识,这是实现Okhttp机制的基础。

2.1、HTTP缓存策略

HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的,最终响应式从缓存中去,还是从服务端重新拉取,HTTP的缓存机制的流程如下所示:

HTTP的缓存可以分为两种:

2.2、强制缓存:

  • 需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。 

2.2.1、强制缓存使用的的两个标识:

  • Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
  • Cache-Control:Expires有个时间校验的问题,所以HTTP1.1采用Cache-Control替代Expires。

2.2.2、Cache-Control的取值

  • private::客户端可以缓存。
  • public:客户端和代理服务器都可缓存。
  • max-age=xxx: 缓存的内容将在 xxx 秒后失效
  • no-cache::需要使用对比缓存来验证缓存数据。
  • no-store:所有内容都不会缓存;强制缓存,对比缓存都不会触发。

2.3、对比缓存:

  • 需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。

2.3.1、对比缓存的两个标识:

  • Last-Modified:表示资源上次修改的时间。

当客户端发送第一次请求时,服务端返回资源上次修改的时间,返回格式例子如下:

Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT

  • If-Modified-Since:服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回200表示需要重新请求资源,否则返回304表示资源没有被修改,可以继续使用缓存。不同于If-Unmodified-SinceIf-Modified-Since只能与GETHEAD一起使用。与组合使用时If-None-Match,将被忽略,除非服务器不支持If-None-Match

客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。

If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT

2.3.2、Etag/If-None-Match

使用资源标识码ETag的方式来标记是否修改,如果标识码发生改变,则说明资源已经被修改,ETag优先级高于Last-Modified

  • ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码,返回例子如下:

ETag: "5694c7ef-24dc"

  • 客户端再次发送,会在header里携带上次服务端返回的资源标识码,返回例子如下:

If-None-Match:"5694c7ef-24dc"

服务端接收到客户端发来的资源标识码,则会与自己当前的资源码进行比较,如果不同,则说明资源已经被修改,则返回200,如果相同则说明资源没有被修改,返回 304,客户端可以继续使用缓存。

2.4、两种换成对比:

  • 强制缓存优先于对比缓存。

三、CacheStrategy

3.1、CacheStrategy的构造函数

CacheStrategy(Request networkRequest, Response cacheResponse) {
  this.networkRequest = networkRequest;
  this.cacheResponse = cacheResponse;
}
  • networkRequest:网络请求。
  • cacheResponse:缓存响应,基于DiskLruCache实现的文件缓存,可以是请求中url的md5,value是文件中查询到的缓存。

CacheStrategy就是利用这两个参数生成最终的策略,有点像map操作,将networkRequest与cacheResponse这两个值输入,处理之后再将这两个值输出,们的组合结果如下所示:

  • 如果networkRequestnullcacheResponsenullonly-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)。
  • 如果networkRequestnullcacheResponsenon-null:不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络。
  • 如果networkRequestnon-nullcacheResponsenull:需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
  • 如果networkRequestnon-nullcacheResponsenon-nullHeader中含有ETag/Last-Modified标签,需要在条件请求下使用,还是需要访问网络。

3.2、以上四种情况的判定

  • CacheStrategy是利用Factory模式进行构造的;
  • CacheStrategy.Factory对象构建以后,调用它的get()方法即可获得具体的CacheStrategy;
  • CacheStrategy.Factory.get()方法内部 调用的是CacheStrategy.Factory.getCandidate()方法,它是核心的实现。

我们先来看一下CacheStrategy.Factory类

3.2.1、Factory类

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;
        } 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.
//1. 如果缓存没有命中,就直接进行网络请求。
    if (cacheResponse == null) {
      return new CacheStrategy(request, null);
    }

    // Drop the cached response if it's missing a required handshake.
//2. 如果TLS握手信息丢失,则返回直接进行连接。
    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.
//如果不应存储此响应,则永远不应将其用作响应源。 只要持久性存储表现良好且规则不变,此检查应该是冗余的。
//3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
    if (!isCacheable(cacheResponse, request)) {
      return new CacheStrategy(request, null);
    }
//4. 如果请求header里有"no-cache"或者有条件GET请求(header里带有ETag/Since标签),则直接连接。
    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);
    }
//计算当前age的时间戳:now - sent + age(Age:对象已在代理缓存中的时间(以秒为单位)。)
    long ageMillis = cacheResponseAge();
//刷新时间,一般服务器设置为max-age
    long freshMillis = computeFreshnessLifetime();

    if (requestCaching.maxAgeSeconds() != -1) {
//一般取max-age
      freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
    }

    long minFreshMillis = 0;
    if (requestCaching.minFreshSeconds() != -1) {
//一般取0
      minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
    }

    long maxStaleMillis = 0;
    if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
      maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
    }
//5. 如果缓存在过期时间内则可以直接使用,则直接返回上次缓存。
    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.
//找到要添加到请求中的条件。如果条件满足,则响应body不会被传送。
//6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求
 //交给服务端判断处理
    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);
  }

  /**
   * Returns the number of milliseconds that the response was fresh for, starting from the served
   * date.
   */
  private long computeFreshnessLifetime() {
    CacheControl responseCaching = cacheResponse.cacheControl();
    if (responseCaching.maxAgeSeconds() != -1) {
      return SECONDS.toMillis(responseCaching.maxAgeSeconds());
    } else if (expires != null) {
      long servedMillis = servedDate != null
          ? servedDate.getTime()
          : receivedResponseMillis;
      long delta = expires.getTime() - servedMillis;
      return delta > 0 ? delta : 0;
    } else if (lastModified != null
        && cacheResponse.request().url().query() == null) {
      // As recommended by the HTTP RFC and implemented in Firefox, the
      // max age of a document should be defaulted to 10% of the
      // document's age at the time it was served. Default expiration
      // dates aren't used for URIs containing a query.
      long servedMillis = servedDate != null
          ? servedDate.getTime()
          : sentRequestMillis;
      long delta = servedMillis - lastModified.getTime();
      return delta > 0 ? (delta / 10) : 0;
    }
    return 0;
  }

  /**
   * Returns the current age of the response, in milliseconds. The calculation is specified by RFC
   * 7234, 4.2.3 Calculating Age.
   */
  private long cacheResponseAge() {
    long apparentReceivedAge = servedDate != null
        ? Math.max(0, receivedResponseMillis - servedDate.getTime())
        : 0;
    long receivedAge = ageSeconds != -1
        ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
        : apparentReceivedAge;
    long responseDuration = receivedResponseMillis - sentRequestMillis;
    long residentDuration = nowMillis - receivedResponseMillis;
    return receivedAge + responseDuration + residentDuration;
  }

  /**
   * Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a
   * cached response older than 24 hours, we are required to attach a warning.
   */
  private boolean isFreshnessLifetimeHeuristic() {
    return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null;
  }

  /**
   * Returns true if the request contains conditions that save the server from sending a response
   * that the client has locally. When a request is enqueued with its own conditions, the built-in
   * response cache won't be used.
   */
  private static boolean hasConditions(Request request) {
    return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
  }
}

整个函数的逻辑就是按照上面那个HTTP缓存判定流程图来实现,具体流程如下所示:

  • 如果缓存没有命中,就直接进行网络请求。
  • 如果TLS握手信息丢失,则返回直接进行连接。
  • 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
  • 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。
  • 如果缓存在过期时间内则可以直接使用,则直接返回上次缓存。
  • 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求交给服务端判断处理

整个流程就是这样,另外说一点,Okhttp的缓存是根据服务器header自动的完成的,整个流程也是根据RFC文档写死的,客户端不必要进行手动控制。

四、缓存管理

Okhttp的缓存机制是基于DiskLruCache做的。Cache类封装了缓存的实现,实现了InternalCache接口。

我们来看一下InternalCache接口:

/**
 * OkHttp的内部缓存接口。应用程序不应该实现这个:而是使用{@link
 * okhttp3.Cache}
 * OkHttp's internal cache interface. Applications shouldn't implement this: instead use {@link
 * okhttp3.Cache}.
 */
public interface InternalCache {
//获取缓存
  Response get(Request request) throws IOException;
//存入缓存
  CacheRequest put(Response response) throws IOException;

  /**
   * Remove any cache entries for the supplied {@code request}. This is invoked when the client
   * invalidates the cache, such as when making POST requests.
   */
//移除缓存
  void remove(Request request) throws IOException;

  /**
   * Handles a conditional request hit by updating the stored cache response with the headers from
   * {@code network}. The cached response body is not updated. If the stored response has changed
   * since {@code cached} was returned, this does nothing.
   */
//更新缓存
  void update(Response cached, Response network);

  /** Track an conditional GET that was satisfied by this cache. */
//跟踪一个满足缓存条件的GET请求
  void trackConditionalCacheHit();

  /** Track an HTTP response being satisfied with {@code cacheStrategy}. */
//跟踪满足缓存策略CacheStrategy的响应
  void trackResponse(CacheStrategy cacheStrategy);
}

接下来我们来看一下他的实现类。

Cache没有直接实现InternalCache这个接口,而是在其构造方法中进行了InternalCache匿名内部类的实现。

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

Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.internalCache = new InternalCache() {
        public Response get(Request request) throws IOException {
            return Cache.this.get(request);
        }

        public CacheRequest put(Response response) throws IOException {
            return Cache.this.put(response);
        }

        public void remove(Request request) throws IOException {
            Cache.this.remove(request);
        }

        public void update(Response cached, Response network) {
            Cache.this.update(cached, network);
        }

        public void trackConditionalCacheHit() {
            Cache.this.trackConditionalCacheHit();
        }

        public void trackResponse(CacheStrategy cacheStrategy) {
            Cache.this.trackResponse(cacheStrategy);
        }
    };
    this.cache = DiskLruCache.create(fileSystem, directory, 201105, 2, maxSize);
}

在Cache类里还定义一些内部类,这些类封装了请求与响应信息。

  • Cache.Entry:封装了请求与响应等信息,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis与receivedResponseMillis。
  • Cache.CacheResponseBody:继承于ResponseBody,封装了缓存快照snapshot,响应体bodySource,内容类型contentType,内容长度contentLength。

Cache.Entry类:

private static final class Entry {
  /** Synthetic response header: the local time when the request was sent. */
  private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";

  /** Synthetic response header: the local time when the response was received. */
  private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";

  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 @Nullable Handshake handshake;
  private final long sentRequestMillis;
  private final long receivedResponseMillis;

  /**
   * Reads an entry from an input stream. A typical entry looks like this:
   * <pre>{@code
   *   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
   * }</pre>
   *
   * <p>A typical HTTPS file looks like this:
   * <pre>{@code
   *   https://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
   *
   *   AES_256_WITH_MD5
   *   2
   *   base64-encoded peerCertificate[0]
   *   base64-encoded peerCertificate[1]
   *   -1
   *   TLSv1.2
   * }</pre>
   * The file is newline separated. The first two lines are the URL and the request method. Next
   * is the number of HTTP Vary request header lines, followed by those lines.
   *
   * <p>Next is the response status line, followed by the number of HTTP response header lines,
   * followed by those lines.
   *
   * <p>HTTPS responses also contain SSL session information. This begins with a blank line, and
   * then a line containing the cipher suite. Next is the length of the peer certificate chain.
   * These certificates are base64-encoded and appear each on their own line. The next line
   * contains the length of the local certificate chain. These certificates are also
   * base64-encoded and appear each on their own line. A length of -1 is used to encode a null
   * array. The last line is optional. If present, it contains the TLS version.
   */
  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();

      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())
            : TlsVersion.SSL_3_0;
        handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates);
      } else {
        handshake = null;
      }
    } finally {
      in.close();
    }
  }

  Entry(Response response) {
    this.url = response.request().url().toString();
    this.varyHeaders = HttpHeaders.varyHeaders(response);
    this.requestMethod = response.request().method();
    this.protocol = response.protocol();
    this.code = response.code();
    this.message = response.message();
    this.responseHeaders = response.headers();
    this.handshake = response.handshake();
    this.sentRequestMillis = response.sentRequestAtMillis();
    this.receivedResponseMillis = response.receivedResponseAtMillis();
  }

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

  private boolean isHttps() {
    return url.startsWith("https://");
  }

  private List<Certificate> readCertificateList(BufferedSource source) throws IOException {
    int length = readInt(source);
    if (length == -1) return Collections.emptyList(); // OkHttp v1.2 used -1 to indicate null.

    try {
      CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
      List<Certificate> result = new ArrayList<>(length);
      for (int i = 0; i < length; i++) {
        String line = source.readUtf8LineStrict();
        Buffer bytes = new Buffer();
        bytes.write(ByteString.decodeBase64(line));
        result.add(certificateFactory.generateCertificate(bytes.inputStream()));
      }
      return result;
    } catch (CertificateException e) {
      throw new IOException(e.getMessage());
    }
  }

  private void writeCertList(BufferedSink sink, List<Certificate> certificates)
      throws IOException {
    try {
      sink.writeDecimalLong(certificates.size())
          .writeByte('\n');
      for (int i = 0, size = certificates.size(); i < size; i++) {
        byte[] bytes = certificates.get(i).getEncoded();
        String line = ByteString.of(bytes).base64();
        sink.writeUtf8(line)
            .writeByte('\n');
      }
    } catch (CertificateEncodingException e) {
      throw new IOException(e.getMessage());
    }
  }

  public boolean matches(Request request, Response response) {
    return url.equals(request.url().toString())
        && requestMethod.equals(request.method())
        && HttpHeaders.varyMatches(response, varyHeaders, request);
  }

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

Cache.CacheResponseBody类:

private static class CacheResponseBody extends ResponseBody {
  final DiskLruCache.Snapshot snapshot;
  private final BufferedSource bodySource;
  private final @Nullable String contentType;
  private final @Nullable String contentLength;

  CacheResponseBody(final DiskLruCache.Snapshot snapshot,
      String contentType, String contentLength) {
    this.snapshot = snapshot;
    this.contentType = contentType;
    this.contentLength = contentLength;

    Source source = snapshot.getSource(ENTRY_BODY);
    bodySource = Okio.buffer(new ForwardingSource(source) {
      @Override public void close() throws IOException {
        snapshot.close();
        super.close();
      }
    });
  }

  @Override public MediaType contentType() {
    return contentType != null ? MediaType.parse(contentType) : null;
  }

  @Override public long contentLength() {
    try {
      return contentLength != null ? Long.parseLong(contentLength) : -1;
    } catch (NumberFormatException e) {
      return -1;
    }
  }

  @Override public BufferedSource source() {
    return bodySource;
  }
}

除了两个类以外,Okhttp还封装了一个文件系统类FileSystem类,这个类利用Okio这个库对Java的FIle操作进行了一层封装,简化了IO操作。理解了这些剩下的就是DiskLruCahe里的插入缓存 、获取缓存和删除缓存的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值