当前位置: 代码迷 >> 综合 >> okhttp 源码解析(二)
  详细解决方案

okhttp 源码解析(二)

热度:79   发布时间:2023-12-12 16:40:30.0

前言

本篇文章,笔者就会来开始分析 okhttp 的重中之重——拦截器链的相关知识了。拦截器链是 okhttp 中设计的非常巧妙的一个机制,对它的分析难度中等,但是它的内容非常之多,希望大家有一定的耐心进行分析。笔者会尽自己所能,将它的原理讲解清楚。在学习本章知识的时候,你需要对 okhttp 的执行流程有一个认识,如果你还未了解执行流程的内部实现机制的话,建议先看这篇文章 okhttp3源码解析(一) 后再来进行本章的学习。本章讲解的是前三个拦截器:RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptor

整体设计

在开始分析源码之前,我们先来从整体上对 okhttp 的拦截器链机制有个整体的认识,先上图:

实际上 okhttp 的拦截器不止我们列举的这 5 个,它还包含有客户端的拦截器和 Web Socket 拦截器,但是在拦截器链分析过程中我们不会对这两个拦截器进行分析,我们只会分析框架内部的这5个拦截器,它们各自的功能如下:

  • RetryAndFollowUpInterceptor:重试和失败重定向拦截器,它主要用于当请求失败时会去尝试重新连接,当然重连次数是有一定次数的。
  • BridgeInterceptor:桥接拦截器,它主要用于补充用户请求中缺少的一些必须的请求头等。
  • CacheInterceptor:缓存拦截器,它主要用于对我们的请求进行缓存,来提高我们的请求效率。
  • ConnectInterceptor:连接拦截器,它的主要作用是负责建立可用的连接来为我们的 HTTP 请求写入网络 IO 流做准备。
  • CallServerInterceptor:请求服务器拦截器,将 HTTP 请求写进网络的 IO 流中,并读取服务端返回给客户端的响应数据。

在介绍完了这5个拦截器的各自功能之后,我们接下来简要说明一下请求的传递顺序。

首先我们的请求就会顺着上图 黑色箭头 所示的方向,从重试和失败重定向拦截器开始,到桥接拦截器、缓存拦截器、连接拦截器,最后到达请求服务器拦截器,在请求服务器拦截器中,我们会将我们的请求发送给服务器。

接着服务器会返回响应给请求服务器拦截器,然后响应就会沿着上图的 红色箭头 一路往上传递,最终到达我们的重试和失败重定向拦截器。这是通过这样子的一个机制,我们把这5个拦截器像一条链子一样串联了起来,所以我们的这个机制才叫做拦截器链机制。

在通过流程图了解了整体的流程之后,我们接下来就通过源代码来分析这条链是如何形成的。首先要说明一点,不同于我们之前的执行流程分析有区分同步和异步,拦截器链机制是不对同步和异步做区分的。所以在这里我们就从 Callexecute 这个同步请求方法入手:

@Override public Response execute() throws IOException {
    synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");executed = true;}captureCallStackTrace();eventListener.callStart(this);try {
    client.dispatcher().executed(this);Response result = getResponseWithInterceptorChain();if (result == null) throw new IOException("Canceled");return result;} catch (IOException e) {
    eventListener.callFailed(this, e);throw e;} finally {
    client.dispatcher().finished(this);}
}

如果之前学习过同步请求的执行流程分析的话,想必这段代码你已经是非常熟悉的了。我们在这里就直接看到代码第10行,在这里我们是通过 getResponseWithInterceptorChain 方法获得请求响应的,那么我们的拦截链机制的分析就是从这个方法开始入手了。我们来看看这个方法里面做了什么:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.List<Interceptor> interceptors = new ArrayList<>();interceptors.addAll(client.interceptors());interceptors.add(retryAndFollowUpInterceptor);interceptors.add(new BridgeInterceptor(client.cookieJar()));interceptors.add(new CacheInterceptor(client.internalCache()));interceptors.add(new ConnectInterceptor(client));if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());}interceptors.add(new CallServerInterceptor(forWebSocket));Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,originalRequest, this, eventListener, client.connectTimeoutMillis(),client.readTimeoutMillis(), client.writeTimeoutMillis());return chain.proceed(originalRequest);
}
}

可以看到,这个方法内部创建了一个 List 用于存放拦截器,将 RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorCallServerInterceptor 都添加了进去。然后看到代码第14行,我们创建了一个 Chain 对象,这就是我们的拦截链了,在它的初始化方法中,我们将我们 interceptors 作为参数传了进去,并且我们留意到第4个参数我们传进去的是 0。那么接下来我们就来看看 Chain 的构造方法里面做了什么:

public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,EventListener eventListener, int connectTimeout, int readTimeout, int writeTimeout) {
    this.interceptors = interceptors;this.connection = connection;this.streamAllocation = streamAllocation;this.httpCodec = httpCodec;this.index = index;this.request = request;this.call = call;this.eventListener = eventListener;this.connectTimeout = connectTimeout;this.readTimeout = readTimeout;this.writeTimeout = writeTimeout;
}

就是一些简单的初始化工作,看到这个第4个参数 index,它在这里代表的是 interceptors 中元素的索引,这里的元素指的就是我们各个拦截器。接下来我们继续回到 getResponseWithInterceptorChain 方法,看到它最后的 return 语句,它调用的是 Chainproceed 方法,并把我们的 Request 作为参数传了进去。因为 Chain 是一个接口,所以我们直接到它的实现类 RealInterceptorChain 中看它的 proceed 方法,源码如下:

@Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
}

调用的是另一个 proceed 方法,我们看到这个 proceed 方法里面做了什么:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,......// Call the next interceptor in the chain.RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,writeTimeout);Interceptor interceptor = interceptors.get(index);Response response = interceptor.intercept(next);......return response;}

为了突出重点把其它的无关代码给省略了,我们看到代码第4行,我们在这里创建了一个 RealInterceptorChain 对象 next,注意到在它的第4个参数我们传进去的是 index+1,也就是我们当前拦截器的下一个拦截器的索引。然后我们继续看到代码第7行,我们获取了当前拦截器的对象,然后在第8行创建了 Response 对象,接收我们当前拦截器调用的 intercept 方法的返回值,这是一个接口方法,在我们所列举的 5 个拦截器中都实现了这个方法。它传入的参数是我们的下一个拦截器 next。所以我们来看看它的实现方法之一 —— RetryAndFollowUpInterceptorintercept 方法里面做了什么:

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();RealInterceptorChain realChain = (RealInterceptorChain) chain;Call call = realChain.call();......Response priorResponse = null;while (true) {
    if (canceled) {
    streamAllocation.release();throw new IOException("Canceled");}Response response;boolean releaseConnection = true;try {
    response = realChain.proceed(request, streamAllocation, null, null);releaseConnection = false;}......}}

我们把无关的代码略去,将重点看到代码第16行,可以看到在这里,我们创建了 Response 对象,接收 proceed 方法的返回值,而 realChain 指的是我们的下一个拦截器链对象,下一个拦截器链继续调用 proceed 方法,就会继续指向下下个拦截器了,这对应的就是上图 黑色箭头 的方向。而我们的 Response 对象则会 return 回我们的上一个拦截器,这对应的就是 红色箭头 的方向。

举一个具体的例子,假设当前的 index 指向的拦截器是 RetryAndFollowUpInterceptor,那么 index+1 指向的拦截器就是 BridgeInterceptor 了。通过 proceed 方法,我们的 RetryAndFollowUpInterceptor 拦截器会调用它自身的 intercept 方法,而这个方法内部又会让 RetryAndFollowUpInterceptor 的下一个拦截器也就是 BridgeInterceptor 调用 proceed 方法继续去调用 BridgeInterceptor 的下一个拦截器 CacheInterceptor

也就是说,okhttp 通过 proceed 方法,巧妙地将我们 List 中的各个拦截器连接成了一条链,接下来我们先来总结一下这个过程:

  1. 创建一系列拦截器,将它们放在一个 List 中。
  2. 创建一个拦截器链 Chain,并执行拦截器链的 proceed 方法。
  3. 调用下一个拦截器的 proceed 方法,获得 Response黑色箭头)。
  4. Response 返回上一个拦截器(红色箭头)。

讲完了这些拦截器是如何串成拦截器链的,接下来我们就按照 List 添加的顺序来逐个分析这些拦截器的源码,可能你会问,这些拦截器是调用的哪个方法处理我们的请求的呢?其实我们在前面已经粗略地分析这个方法了,它就是每个拦截器中的 intercept 方法,在前面我们只简要地分析了它如何串成链,接下来我们就来看它们各自的逻辑是什么样子的。首先就是我们的重试和失败重定向拦截器。

RetryAndFollowUpInterceptor

这个拦截器叫做重试和失败重定向拦截器,从名字上我们也能知道这个拦截器主要是用于我们的失败重连的,那么我们来看看它的 intercept 方法里面做了什么:

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();RealInterceptorChain realChain = (RealInterceptorChain) chain;Call call = realChain.call();EventListener eventListener = realChain.eventListener();StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),createAddress(request.url()), call, eventListener, callStackTrace);this.streamAllocation = streamAllocation;int followUpCount = 0;Response priorResponse = null;while (true) {
    if (canceled) {
    streamAllocation.release();throw new IOException("Canceled");}Response response;boolean releaseConnection = true;try {
    response = realChain.proceed(request, streamAllocation, null, null);releaseConnection = false;} catch (RouteException e) {
    // The attempt to connect via a route failed. The request will not have been sent.if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
    throw e.getLastConnectException();}releaseConnection = false;continue;} catch (IOException e) {
    // An attempt to communicate with a server failed. The request may have been sent.boolean requestSendStarted = !(e instanceof ConnectionShutdownException);if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;releaseConnection = false;continue;} finally {
    // We're throwing an unchecked exception. Release any resources.if (releaseConnection) {
    streamAllocation.streamFailed(null);streamAllocation.release();}}// Attach the prior response if it exists. Such responses never have a body.if (priorResponse != null) {
    response = response.newBuilder().priorResponse(priorResponse.newBuilder().body(null).build()).build();}Request followUp = followUpRequest(response, streamAllocation.route());if (followUp == null) {
    if (!forWebSocket) {
    streamAllocation.release();}return response;}closeQuietly(response.body());if (++followUpCount > MAX_FOLLOW_UPS) {
    streamAllocation.release();throw new ProtocolException("Too many follow-up requests: " + followUpCount);}......}
}

这段代码有点长,但是它的逻辑其实比较简单。我们把重点看到代码第7行的 StreamAllocation 上来,从名字上我们大概可以猜出它应该是一个流分配对象,它是用于建立执行 HTTP 请求所需的网络组件,需要注意的一点是,虽然我们在这里创建了这个对象,但是在这一层上面我们并不会使用到它,它要跟着拦截器链一路传递到 ConnectInterceptor 后才能发挥作用。

接下来我们继续往下看到代码第11行,看到这个 int 类型的局部变量 followUpCount,它是用于记录我们的失败重连次数的。紧接着我们继续看到第13行的 while 循环里面,这个 while 循环内部就是我们处理逻辑的地方。首先看到代码第14行的 if 判断,如果我们的请求已经被关闭了,StreamAllocation 就会调用 release 方法释放资源,并且抛出异常。

继续往下看到代码第20行的 boolean 型变量 releaseConnection,它是用来判断是否需要释放我们这次连接的,如果我们在下面的 try 语句块中执行 proceed 方法的过程中不会抛出异常,那么它就会被赋值为 false,表明要保持这次连接。如果我们在执行 proceed 方法的过程中抛出异常了,那么在异常处理中我们会去调用 recover 方法进行尝试重新请求,如果重连成功的话,releaseConnection 也会被赋值为 false。最后在 finally 块里面,如果 releaseConnection 值为true 的话,也就意味着我们不会保持这次连接了,我们就直接调用 StreamAllocationrelease 方法释放资源。

再接下来我们看到代码第60行,如果我们顺利地拿到了返回的响应,那么就会将它 return,否则的话,就会在代码第63行将我们 response 的响应主体关闭。最后我们就看到代码第65行,在这个 if 判断中,如果我们的 followUpCount 小于 MAX_FOLLOW_UPS,那么我们就会继续在 while 循环里面尝试重连,此时 followUpCount 数值 +1;否则的话,就调用 StreamAllocationrelease 方法释放资源,因为我们的重连是有次数限制的,超过了这个次数限制就不会再进行尝试重连了。MAX_FOLLOW_UPS 的默认值为 20。

到这里,我们重连和失败重定向拦截器的 intercept 方法就分析完了,我们对这个方法来做一个小结:

  1. 创建 StreamAllocation 对象,这个对象在重连和失败重定向拦截器中创建,但是在连接拦截器中才会被使用到。
  2. while 中调用 proceed 方法进行网络请求,如果请求过程中产生异常就尝试进行重新请求。
  3. 如果请求成功,就会获得响应 Response,当我们的 Response 可用时,会直接返回,否则的话会释放 Response 的响应体,然后进行尝试重连。
  4. 尝试重连有一定次数限制,默认值为 20,如果超过这个次数仍然无法连接成功,那么就会调用 StreamAllocationrelease 方法释放资源。

分析完了 RetryAndFollowUpInterceptor,接下来我们来分析下一个拦截器 BridgeInterceptor 的源码。

BridgeInterceptor

这个拦截器叫做桥接拦截器,它的主要作用是将我们的请求 Request 添加必要的头部信息以及将返回的 Response 做相应的转换这一工作。我们直接来看到 BridgeInterceptorintercept 方法的源码:

@Override public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();Request.Builder requestBuilder = userRequest.newBuilder();RequestBody body = userRequest.body();if (body != null) {
    MediaType contentType = body.contentType();if (contentType != null) {
    requestBuilder.header("Content-Type", contentType.toString());}long contentLength = body.contentLength();if (contentLength != -1) {
    requestBuilder.header("Content-Length", Long.toString(contentLength));requestBuilder.removeHeader("Transfer-Encoding");} else {
    requestBuilder.header("Transfer-Encoding", "chunked");requestBuilder.removeHeader("Content-Length");}}if (userRequest.header("Host") == null) {
    requestBuilder.header("Host", hostHeader(userRequest.url(), false));}if (userRequest.header("Connection") == null) {
    requestBuilder.header("Connection", "Keep-Alive");}// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing// the transfer stream.boolean transparentGzip = false;if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true;requestBuilder.header("Accept-Encoding", "gzip");}List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());if (!cookies.isEmpty()) {
    requestBuilder.header("Cookie", cookieHeader(cookies));}if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());}Response networkResponse = chain.proceed(requestBuilder.build());HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);if (transparentGzip&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))&& HttpHeaders.hasBody(networkResponse)) {
    GzipSource responseBody = new GzipSource(networkResponse.body().source());Headers strippedHeaders = networkResponse.headers().newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build();responseBuilder.headers(strippedHeaders);String contentType = networkResponse.header("Content-Type");responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));}return responseBuilder.build();
}

可以看到这个方法的代码也是比较长的,不过它的逻辑也比较的清晰。从代码第8行的 if 判断开始到代码第45行,都是在为用户的 Request 添加必要的头部信息,使这个 Request 成为一个可进行网络访问的请求,因为我们之前建立好的 Request 其实还不完整,没有达到能发送请求的条件。

我们从 if 判断中可以得知它会为我们设置内容类型、内容长度、编码方式、host、cookie 等头部信息,我们把重点看到代码第26行的 if 判断,在这里我们设置的是连接复用,我们在这里会为我们的连接保活,以备我们下次的复用。复用功能会在缓存拦截器中涉及,这里我们主要知道有这么个设置就好。

接下来看到代码第47行,在这里我们创建了一个 Response 对象 networkResponse,再次通过 proceed 方法来获得我们的响应。这里的 Chain 代表的是我们桥接拦截器的下一个拦截器。

我们再继续往下看,在代码第54行,可以看到这个 if 的判断是比较复杂的,这是一个 gzip 压缩的一个判断。首先我们看到变量 transparentGzip,如果它为 true,表明我们是支持 gzip 解压的。紧接着看到第二个条件,我们会检查取得的响应 networkResponse 的编码方式是否为 gzip,返回 true 则表示我们得到的响应确实是以 gzip 方式编码的。最后是第三个条件,判断我们的响应是否有响应主体,有的话就会返回 true。如果这三个条件都满足的话,我们就会将 networkResponse 进行解压,以便最后呈现给用户的响应内容就是解压后的内容,如果不满足的话,就直接返回 Response

分析完了桥接拦截器的 intercept 的代码之后,我们简单的做一个总结:

  1. 桥接拦截器会将 Request 添加必要的头部信息,使之成为一个可以进行网络访问的请求,在头部信息中我们会为它添加保活以便复用。
  2. 桥接拦截器会将可进行网络访问的 Request 送到下一个拦截器做处理,然后自己会创建一个 Response 对象准备接收返回的响应。
  3. 接收到的响应首先会判断是否为 gzip 编码方式,如果是的话会对响应进行解码再返回,否则的话直接返回,这一步是为了保证用户得到的响应就是解码后的响应。

桥接拦截器的 intercept 方法也是比较好分析的吧,接下来就到了我们的缓存拦截器了,这是本篇文章最难啃的一块骨头了,大家有个心理准备。

CacheInterceptor

这个拦截器叫做缓存拦截器,它的作用其实是比较简单的,就是让我们的下一次请求能够节省更多的时间,直接从缓存中获得数据。既然有缓存,那就肯定是有写缓存和读缓存的方法了,它们分别对应了我们类 Cache 中的 put 方法和 get 方法,我们先来看看这两个方法是如何实现缓存的读写的,再去分析 intercept 方法。

put

put 方法顾名思义就是写缓存的方法了,它的源码如下所示:

@Nullable
CacheRequest put(Response response) {
    String requestMethod = response.request().method();if (HttpMethod.invalidatesCache(response.request().method())) {
    try {
    this.remove(response.request());} catch (IOException var6) {
    ;}return null;} else if (!requestMethod.equals("GET")) {
    return null;} else if (HttpHeaders.hasVaryAll(response)) {
    return null;} else {
    Cache.Entry entry = new Cache.Entry(response);Editor editor = null;try {
    editor = this.cache.edit(key(response.request().url()));if (editor == null) {
    return null;} else {
    entry.writeTo(editor);return new Cache.CacheRequestImpl(editor);}} catch (IOException var7) {
    this.abortQuietly(editor);return null;}}
}

首先看到代码第3行,我们会先去获取请求 Request 的请求方法类型。紧接着看到代码第12行,在这个 else if 判断中,我们会判断我们的请求方法是否为 get,不是 get 方法的话直接返回 null,不做缓存。至于为什么,我在这里只能做个大概的猜测:其他方法例如 post 的缓存太过复杂,所以需要容量较大,会使我们的缓存意义不大。

接下来我们直接跳到代码第16行,如果上面的条件判断都不满足的话,就会走到这个 else 块中,这里面就会做真正的缓存的写入操作了。我们看到代码第17行,在这里我们创建了一个 Entry 对象,我们先来看看 Entry 这个类里面有什么:

private static final class Entry {
    ......private final String url;   // 网络请求 urlprivate 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;  // 接收时间点......
}

我们可以看到,它相当于是一个包装类,将我们需要写入缓存的信息包装到一个类中去维护,可以看到里面有 url、请求头、请求方法、状态码等等。所以 Entry 对象相当于包含了我们需要写入缓存的信息。

我们继续回到 put 方法的第18行,在这里又创建了一个新对象 EditorEditor 类是 DiskLruCache的一个内部类,所以我们的缓存实际上是用 DiskLruCache 的算法进行缓存写入的,Editor 相当于 DiskLruCache 的编辑器,主要用于写入。知道了它的作用之后,我们看到 put 方法的第20行,在这个 try 语块我们就要为 Editor 对象赋值了,我们调用的是 cacheedit 方法,其中,cacheDiskLruCache 类型的对象,edit 方法传入的参数为 key 方法的返回值,而在这里我们的 key 方法处理的 request 方法中的 url,我们来看看 key 方法里面做了什么:

public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
}

可以看到,这个方法返回的就是我们 urlmd5 加密后返回的16进制数,所以我们的 edit 方法传进的参数也就是 urlmd5 加密后的16进制数了。在创建好 Editor 对象后,如果它不为空的话,我们就会执行 EntrywriteTo 方法,这是真正执行写入的方法,我们来看看这个方法的内部实现:

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、请求头、协议、状态码、响应头以及收发时间做一个写入操作,并且在最后一个 if 判断中我们可以看到,他会判断这个请求是不是 HTTPS 类型的请求,如果是的话它才会进行相应的握手操作写入。在分析完 EntrywriteTo 方法之后,我们会发现,这里怎么都只写入了头部信息以及其他信息的缓存,而没有写入最为关键的响应主体呢?

我们顺着 put 方法的代码继续往下看,看到第26行,我们会返回 Cache 的内部类 CacheRequestImpl 的对象 ,在这个内部类中,其实就做了响应主体的写入操作,我们来看看这个内部类是如何定义的:

private final class CacheRequestImpl implements CacheRequest {
    private final Editor editor;private Sink cacheOut;private Sink body;  // 响应主体boolean done;CacheRequestImpl(final Editor editor) {
    this.editor = editor;this.cacheOut = editor.newSink(1);this.body = new ForwardingSink(this.cacheOut) {
    public void close() throws IOException {
    Cache var1 = Cache.this;synchronized(Cache.this) {
    if (CacheRequestImpl.this.done) {
    return;}CacheRequestImpl.this.done = true;++Cache.this.writeSuccessCount;}super.close();editor.commit();}};}......
}

可以看到这个类是继承自 CacheRequest 的,这个类内部维护了一个 Sink 类型的成员变量 body,它其实就是我们的响应主体。它在构造方法中执行对响应主体的写入操作。在分析完 CacheRequest 这个类之后,我们再一次回到我们的 put 方法,为了方便观看我帮它粘了过来:

@Nullable
CacheRequest put(Response response) {
    String requestMethod = response.request().method();if (HttpMethod.invalidatesCache(response.request().method())) {
    ......} else {
    Cache.Entry entry = new Cache.Entry(response);Editor editor = null;try {
    editor = this.cache.edit(key(response.request().url()));if (editor == null) {
    return null;} else {
    entry.writeTo(editor);return new Cache.CacheRequestImpl(editor);}} catch (IOException var7) {
    this.abortQuietly(editor);return null;}}
}

在第16行,可以看到在构造好我们的 CacheRequestImpl 对象之后,直接就将它进行了 return,这个对象的内部储存着我们的响应主体。到这里,我们的 put 方法就分析结束了,我们先来简单地为 put 方法做个总结:

  • 缓存存在限制,只会对 GET 请求方法进行的请求和响应进行缓存。
  • 缓存所使用的写入算法是 DiskLruCache 算法,缓存对象的 key 值则是通过 md5 加密算法对 Requesturl 进行加密生成的16进制数组成的。
  • 缓存的对象有 Request 的请求头、 url、协议、状态码以及响应头和响应主体。

在总结完 put 方法的功能之后,接下来就到了我们的 get 方法的分析。

get

get 方法就是从缓存中读取数据的方法了,它的源代码如下所示:

@Nullable
Response get(Request request) {
    String key = key(request.url());Snapshot snapshot;try {
    snapshot = this.cache.get(key);if (snapshot == null) {
    return null;}} catch (IOException var7) {
    return null;}Cache.Entry entry;try {
    entry = new Cache.Entry(snapshot.getSource(0));} catch (IOException var6) {
    Util.closeQuietly(snapshot);return null;}Response response = entry.response(snapshot);if (!entry.matches(request, response)) {
    Util.closeQuietly(response.body());return null;} else {
    return response;}
}

首先我们根据传入参数 Requesturl,通过与在 put 方法中同样使用的的 key 方法来获取 Requestkey 值。紧接着在第5行,我们创建了一个 Snapshot 对象,Snapshot 我们可以理解为缓存快照,因为我们不同的请求的缓存是不同的,所以每个请求的缓存快照也是不同的。缓存快照就相当于缓存的 id,通过它我们能获取我们想要的缓存。接下来我们就通过 cacheget 方法来获取 Snapshot 的实例,cache 我们在 put 方法也有提到过,它是 DiskLruCache 类型的变量。

继续往后看,如果 Shapshot 对象不为空的话,我们就创建一个 Entry 对象,Entry 我们之前也分析过,主要保存的是我们请求头、响应头以及状态码等信息。我们的 Entry 对象通过 SnapshotgetSource 方法来获得 Entry 对象的实例,如果 Entry 对象也不为空的话,在第23行,我们就会创建 Response 对象,并通过 Entryresponse 方法来为 Response 对象赋值。

最后我们看到第24行的 if 判断语句,这里进行的主要是请求和响应的匹配判断,一个请求只能对应一个响应,如果它们能够匹配的话,才会返回 Response 对象,否则还是会返回 null。到这里 get 方法就分析完了,我们同样来对它进行一个总结:

  • get 方法首先会通过 key 方法来获取相对应缓存的 key 值。
  • 接着通过 key 值获取 Snapshot 对象,它是缓存快照,通过它我们又可以获得 Entry 对象。
  • 获取 Entry 对象后,我们通过它的 response 方法来获取我们缓存中的 Response,取得的 Response 还要与 Request 进行匹配判断,因为一个请求只能对应一个响应,匹配的话才能返回 Response 对象。

在我们知道了 put 方法和 get 方法各自的原理之后,我们最后就来看看缓存拦截器的 intercept 方法是如何实现缓存的读写的吧。

intercept

CacheInterceptorintercept 方法是我们本篇文章中最长的方法了,笔者将会把它分为两部分来进行分析,我们先看到上半部分的源码:

@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.}// If we're forbidden from using the network and the cache is insufficient, fail.if (networkRequest == null && cacheResponse == null) {
    return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(Util.EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();}// If we don't need the network, we're done.if (networkRequest == null) {
    return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();}......
}

在第2行,我们首先会创建一个 Response 的对象 cacheCandidate,如果缓存不为空的话,就会通过 get 方法来尝试获得缓存。接下来我们看到代码第8行,这里我们创建了 CacheStrategy 的对象,这是一个缓存策略类,我们来看看这个类是如何定义的:

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;CacheStrategy(Request networkRequest, Response cacheResponse) {
    this.networkRequest = networkRequest;this.cacheResponse = cacheResponse;}......
}

可以看到 CacheStrategy 维护看两个成员变量 networkRequestcacheResponse,这两个变量来共同决定是使用网络请求还是缓存响应。我们继续回到 intercept 方法的第8行,可以看到 cacheStrategy 是通过 CacheStrategy 内部工厂类的 get 方法构造的,那么我们就来看看这个 get 方法里面做了什么:

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

可以看到这是方法内部最终返回了 candidate 对象,而这个对象又是通过 getCandidate 方法进行赋值的,那么我们就来看看 getCandidate 里面做了什么:

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

这个方法同样非常的长,我们一步步来分析。首先看到第3行的 if 判断,如果 cacheResponse 为空,说明不存在缓存响应,返回的是 new CacheStrategy(request, null)CacheStrategy 的构造方法我们可以回过头看一下,第一个参数是 Request,表明是网络请求对象;第二个参数是 Response,表明是缓存响应对象,我们在这里传入的是 requestnull,表明直接进行网络请求。

接下来看到第8行的 if 判断,首先判断该请求是否为 HTTPS 类型请求,如果为 true 会紧接着判断是否有进行过握手操作,如果没有经历过握手操作的话表明该缓存不可用,和前一个 if 判断一样,也是直接进行网络请求。

继续看到第15行的 if 判断,这个 if 用于判断是否是可缓存的,如果是不可缓存的话,也是直接进行网络请求。

再继续看到第20行的 if 判断语句 if (requestCaching.noCache() || hasConditions(request)),这两个条件只要满足任一一个这个 if 就为真。第一个条件是判断我们的请求是否有缓存,没有缓存返回 true;第二个条件是中的 hasConditions 方法是用于判断这个请求是否是可选择的,如果是可选择的话也返回 true。如果满足这两个条件其中一个的话,我们也会直接进行网络请求。

最后我们看到第25行的 if 判断,通过 immutable 方法来判断请求是不是稳定的,如果是的话,我们就会执行下面的 return new CacheStrategy(null, cacheResponse),这时候表示返回的就是我们的缓存响应。

我们这里对 getCandidate 方法的分析就先告一段落,我们从上面的分析知道了,这个方法主要是根据一系列的判断来决定使用网络请求还是缓存响应。当然它后面还有许多的 if 判断,这里限于篇幅关系就不再一一列举了。最终 getCandidate 方法会返回给我们一个做好决策的 CacheStrategy 对象,拿到这个对象后,我们再次回到 CacheStrategy 内部工厂类的 get 方法中:

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

可以看到,condidate 通过 getCandidate 对象赋值之后,在接下来的 if 判断中,根据注释我们可以知道这是为了防止出现网络不可用并且缓存也存在缺陷的情况。如果不存在这个问题,那么我们直接返回我们的 condidate 对象。分析完了 get 方法,我们总结一下它的用法就是:根据一系列的条件返回一个合适的 CacheStraegy(缓存策略)对象。接下来我们继续回到 get 的上一个方法,也就是我们的 intercept 方法,为了方便观看这里我就直接把它贴过来了:

@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.}// If we're forbidden from using the network and the cache is insufficient, fail.if (networkRequest == null && cacheResponse == null) {
    return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(Util.EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();}// If we don't need the network, we're done.if (networkRequest == null) {
    return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();}......
}

看到我们的第9行,在获取到 CacheStrategy 对象后,接下来我们便通过 RequestResponse 对象来获取 CacheStrategy 的网络请求和缓存响应。

我们继续看到第17行,在这个 if 判断中如果 cacheCandidate 不为空并且 cacheResponse 为空的话,说明没有可用的缓存,那么我们就会关闭 cacheCandidate

继续看到第22行,在这个 if 判断中,如果网络请求和缓存响应都为空,那么我们就会通过构建者模式构造一个 Response 对象出来,注意到它的状态码 code 是 504,即表示网络超时。

最后我们看到第35行的 if 判断,这时如果仅仅是网络请求为空,也就是网络请求不可用的情况下,我们就会使用我们的缓存请求。这里我们不用担心 cacheResponse 也为空,因为在第22行的 if 中它已经判断过了,所以它肯定不为空。

至此,intercept 方法的上半部分源码就分析完了,总的来说这部分的工作就是首先尝试去获取缓存,接着创建 CacheStrategy 对象获取网络请求和缓存响应,如果网络请求不可用,无论是否有缓存,都会 return 我们的 Response。接下来我们来看看下半部分的源码:

@Override public Response intercept(Chain chain) throws IOException {
    ......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) {
    Response response = cacheResponse.newBuilder().headers(combine(cacheResponse.headers(), networkResponse.headers())).sentRequestAtMillis(networkResponse.sentRequestAtMillis()).receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();networkResponse.body().close();// Update the cache after combining headers but before stripping the// Content-Encoding header (as performed by initContentStream()).cache.trackConditionalCacheHit();cache.update(cacheResponse, response);return response;} else {
    closeQuietly(cacheResponse.body());}}Response response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();if (cache != null) {
    if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // Offer this request to the cache.CacheRequest cacheRequest = cache.put(response);return cacheWritingResponse(cacheRequest, response);}if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
    cache.remove(networkRequest);} catch (IOException ignored) {
    // The cache cannot be written.}}}return response;
}

在上半部分的源码中,我们的网络请求不可用,无论我们的缓存可用与否,都会返回一个 Response,能执行到下半部分的源码,说明我们的网络是可用的,那么我们便会进行网络请求。可以看到在第5行的 try 语句块中,我们又见到了熟悉的 proceed 方法,就是把我们的 Request 送到下一个拦截器去处理,然后取得返回的响应赋值给 networkResponse。在 finally 语块中,如果我们的 networkResponse 为空并且 cacheCandidate 不为空的话,就会关闭我们的 networkResponsebody

接下来我们看到第14行的 if 判断,这是个很关键的点,最外层 if 中,判断是否有存在缓存响应,如果有缓存响应的话,我们继续看到第15行的内层 if 判断,它会判断 networkResponse 的状态码是否为 HTTP_NOT_MODIFIED,即未被修改,如果相等的话,那么我们就直接通过构建者模式返回我们的缓存响应。否则的话,我们就会在第35行的地方通过网络响应来构建我们的响应,并在第40行的 if 判断中判断是否可缓存,可缓存的话就会通过 put 方法,将我们的网络缓存 networkResponse 写入到我们的缓存当中,然后进行返回。

至此,我们 CacheInterceptorintercept 方法就全部分析完了,这个方法的代码非常的多,但其实不难理解,我们下面总结一下它的功能:

  • 在允许缓存的时候,首先尝试去获取缓存,然后在通过创建 CacheStrategy 对象来进行缓存决策。
  • 在网络请求不可用的情况下,有缓存响应的话就会直接返回缓存响应,如果没有缓存响应就会返回状态码 504,表示网络超时。
  • 在网络可用的情况下,我们会进行网络请求,通过 proceed 方法将 Request 交给下一个拦截器处理,接着判断返回的响应的状态码是否为 HTTP_NOT_MODIFIED,如果是的话直接返回缓存响应,这能提高效率。
  • 如果网络响应状态码是其他类型的话,那么我们就要使用我们的网络响应,在返回之前我们会先判断是否为可缓存,如果为可缓存的话会将它先写入缓存之后再返回。

关于拦截器链的知识,我们在本篇就先介绍到这里了,在下一篇博客 okhttp源码解析(三) 中笔者会继续分析后两个拦截器 ConnectInterceptorCallServerInterceptor,有问题的话可以在下方的评论区给我留言,祝大家学习愉快!

  相关解决方案