本篇文章,笔者就会来开始分析 okhttp
的重中之重——拦截器链的相关知识了。拦截器链是 okhttp
中设计的非常巧妙的一个机制,对它的分析难度中等,但是它的内容非常之多,希望大家有一定的耐心进行分析。笔者会尽自己所能,将它的原理讲解清楚。在学习本章知识的时候,你需要对 okhttp
的执行流程有一个认识,如果你还未了解执行流程的内部实现机制的话,建议先看这篇文章 okhttp3源码解析(一) 后再来进行本章的学习。本章讲解的是前三个拦截器:RetryAndFollowUpInterceptor
、BridgeInterceptor
和 CacheInterceptor
。
在开始分析源码之前,我们先来从整体上对 okhttp
的拦截器链机制有个整体的认识,先上图:
实际上 okhttp
的拦截器不止我们列举的这 5 个,它还包含有客户端的拦截器和 Web Socket 拦截器,但是在拦截器链分析过程中我们不会对这两个拦截器进行分析,我们只会分析框架内部的这5个拦截器,它们各自的功能如下:
在介绍完了这5个拦截器的各自功能之后,我们接下来简要说明一下请求的传递顺序。
首先我们的请求就会顺着上图 黑色箭头 所示的方向,从重试和失败重定向拦截器开始,到桥接拦截器、缓存拦截器、连接拦截器,最后到达请求服务器拦截器,在请求服务器拦截器中,我们会将我们的请求发送给服务器。
接着服务器会返回响应给请求服务器拦截器,然后响应就会沿着上图的 红色箭头 一路往上传递,最终到达我们的重试和失败重定向拦截器。这是通过这样子的一个机制,我们把这5个拦截器像一条链子一样串联了起来,所以我们的这个机制才叫做拦截器链机制。
在通过流程图了解了整体的流程之后,我们接下来就通过源代码来分析这条链是如何形成的。首先要说明一点,不同于我们之前的执行流程分析有区分同步和异步,拦截器链机制是不对同步和异步做区分的。所以在这里我们就从 Call
的 execute
这个同步请求方法入手:
@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
用于存放拦截器,将 RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
、ConnectInterceptor
和 CallServerInterceptor
都添加了进去。然后看到代码第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
语句,它调用的是 Chain
的 proceed
方法,并把我们的 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
。所以我们来看看它的实现方法之一 —— RetryAndFollowUpInterceptor
的 intercept
方法里面做了什么:
@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
中的各个拦截器连接成了一条链,接下来我们先来总结一下这个过程:
List
中。Chain
,并执行拦截器链的 proceed
方法。proceed
方法,获得 Response
(黑色箭头)。Response
返回上一个拦截器(红色箭头)。讲完了这些拦截器是如何串成拦截器链的,接下来我们就按照 List
添加的顺序来逐个分析这些拦截器的源码,可能你会问,这些拦截器是调用的哪个方法处理我们的请求的呢?其实我们在前面已经粗略地分析这个方法了,它就是每个拦截器中的 intercept
方法,在前面我们只简要地分析了它如何串成链,接下来我们就来看它们各自的逻辑是什么样子的。首先就是我们的重试和失败重定向拦截器。
这个拦截器叫做重试和失败重定向拦截器,从名字上我们也能知道这个拦截器主要是用于我们的失败重连的,那么我们来看看它的 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
的话,也就意味着我们不会保持这次连接了,我们就直接调用 StreamAllocation
的 release
方法释放资源。
再接下来我们看到代码第60行,如果我们顺利地拿到了返回的响应,那么就会将它 return
,否则的话,就会在代码第63行将我们 response
的响应主体关闭。最后我们就看到代码第65行,在这个 if
判断中,如果我们的 followUpCount
小于 MAX_FOLLOW_UPS
,那么我们就会继续在 while
循环里面尝试重连,此时 followUpCount
数值 +1;否则的话,就调用 StreamAllocation
的 release
方法释放资源,因为我们的重连是有次数限制的,超过了这个次数限制就不会再进行尝试重连了。MAX_FOLLOW_UPS
的默认值为 20。
到这里,我们重连和失败重定向拦截器的 intercept
方法就分析完了,我们对这个方法来做一个小结:
StreamAllocation
对象,这个对象在重连和失败重定向拦截器中创建,但是在连接拦截器中才会被使用到。while
中调用 proceed
方法进行网络请求,如果请求过程中产生异常就尝试进行重新请求。Response
,当我们的 Response
可用时,会直接返回,否则的话会释放 Response
的响应体,然后进行尝试重连。StreamAllocation
的 release
方法释放资源。分析完了 RetryAndFollowUpInterceptor
,接下来我们来分析下一个拦截器 BridgeInterceptor
的源码。
这个拦截器叫做桥接拦截器,它的主要作用是将我们的请求 Request
添加必要的头部信息以及将返回的 Response
做相应的转换这一工作。我们直接来看到 BridgeInterceptor
的intercept
方法的源码:
@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
的代码之后,我们简单的做一个总结:
Request
添加必要的头部信息,使之成为一个可以进行网络访问的请求,在头部信息中我们会为它添加保活以便复用。Request
送到下一个拦截器做处理,然后自己会创建一个 Response
对象准备接收返回的响应。gzip
编码方式,如果是的话会对响应进行解码再返回,否则的话直接返回,这一步是为了保证用户得到的响应就是解码后的响应。桥接拦截器的 intercept
方法也是比较好分析的吧,接下来就到了我们的缓存拦截器了,这是本篇文章最难啃的一块骨头了,大家有个心理准备。
这个拦截器叫做缓存拦截器,它的作用其实是比较简单的,就是让我们的下一次请求能够节省更多的时间,直接从缓存中获得数据。既然有缓存,那就肯定是有写缓存和读缓存的方法了,它们分别对应了我们类 Cache
中的 put
方法和 get
方法,我们先来看看这两个方法是如何实现缓存的读写的,再去分析 intercept
方法。
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; // 网络请求 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; // 接收时间点
......
}
我们可以看到,它相当于是一个包装类,将我们需要写入缓存的信息包装到一个类中去维护,可以看到里面有 url、请求头、请求方法、状态码等等。所以 Entry
对象相当于包含了我们需要写入缓存的信息。
我们继续回到 put
方法的第18行,在这里又创建了一个新对象 Editor
,Editor
类是 DiskLruCache
的一个内部类,所以我们的缓存实际上是用 DiskLruCache
的算法进行缓存写入的,Editor
相当于 DiskLruCache
的编辑器,主要用于写入。知道了它的作用之后,我们看到 put
方法的第20行,在这个 try
语块我们就要为 Editor
对象赋值了,我们调用的是 cache
的 edit
方法,其中,cache
是 DiskLruCache
类型的对象,edit
方法传入的参数为 key
方法的返回值,而在这里我们的 key
方法处理的 request
方法中的 url
,我们来看看 key
方法里面做了什么:
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
可以看到,这个方法返回的就是我们 url
经 md5
加密后返回的16进制数,所以我们的 edit
方法传进的参数也就是 url
经 md5
加密后的16进制数了。在创建好 Editor
对象后,如果它不为空的话,我们就会执行 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');
}
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
类型的请求,如果是的话它才会进行相应的握手操作写入。在分析完 Entry
的 writeTo
方法之后,我们会发现,这里怎么都只写入了头部信息以及其他信息的缓存,而没有写入最为关键的响应主体呢?
我们顺着 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
加密算法对 Request
的 url
进行加密生成的16进制数组成的。Request
的请求头、 url
、协议、状态码以及响应头和响应主体。在总结完 put
方法的功能之后,接下来就到了我们的 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;
}
}
首先我们根据传入参数 Request
的 url
,通过与在 put
方法中同样使用的的 key
方法来获取 Request
的 key
值。紧接着在第5行,我们创建了一个 Snapshot
对象,Snapshot
我们可以理解为缓存快照,因为我们不同的请求的缓存是不同的,所以每个请求的缓存快照也是不同的。缓存快照就相当于缓存的 id
,通过它我们能获取我们想要的缓存。接下来我们就通过 cache
的 get
方法来获取 Snapshot
的实例,cache
我们在 put
方法也有提到过,它是 DiskLruCache
类型的变量。
继续往后看,如果 Shapshot
对象不为空的话,我们就创建一个 Entry
对象,Entry
我们之前也分析过,主要保存的是我们请求头、响应头以及状态码等信息。我们的 Entry
对象通过 Snapshot
的 getSource
方法来获得 Entry
对象的实例,如果 Entry
对象也不为空的话,在第23行,我们就会创建 Response
对象,并通过 Entry
的 response
方法来为 Response
对象赋值。
最后我们看到第24行的 if
判断语句,这里进行的主要是请求和响应的匹配判断,一个请求只能对应一个响应,如果它们能够匹配的话,才会返回 Response
对象,否则还是会返回 null
。到这里 get
方法就分析完了,我们同样来对它进行一个总结:
get
方法首先会通过 key
方法来获取相对应缓存的 key
值。key
值获取 Snapshot
对象,它是缓存快照,通过它我们又可以获得 Entry
对象。Entry
对象后,我们通过它的 response
方法来获取我们缓存中的 Response
,取得的 Response
还要与 Request
进行匹配判断,因为一个请求只能对应一个响应,匹配的话才能返回 Response
对象。在我们知道了 put
方法和 get
方法各自的原理之后,我们最后就来看看缓存拦截器的 intercept
方法是如何实现缓存的读写的吧。
CacheInterceptor
的 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();
}
......
}
在第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
维护看两个成员变量 networkRequest
和 cacheResponse
,这两个变量来共同决定是使用网络请求还是缓存响应。我们继续回到 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
,表明是缓存响应对象,我们在这里传入的是 request
和 null
,表明直接进行网络请求。
接下来看到第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
对象后,接下来我们便通过 Request
和 Response
对象来获取 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
不为空的话,就会关闭我们的 networkResponse
的 body
。
接下来我们看到第14行的 if
判断,这是个很关键的点,最外层 if
中,判断是否有存在缓存响应,如果有缓存响应的话,我们继续看到第15行的内层 if
判断,它会判断 networkResponse
的状态码是否为 HTTP_NOT_MODIFIED
,即未被修改,如果相等的话,那么我们就直接通过构建者模式返回我们的缓存响应。否则的话,我们就会在第35行的地方通过网络响应来构建我们的响应,并在第40行的 if
判断中判断是否可缓存,可缓存的话就会通过 put
方法,将我们的网络缓存 networkResponse
写入到我们的缓存当中,然后进行返回。
至此,我们 CacheInterceptor
的 intercept
方法就全部分析完了,这个方法的代码非常的多,但其实不难理解,我们下面总结一下它的功能:
CacheStrategy
对象来进行缓存决策。proceed
方法将 Request
交给下一个拦截器处理,接着判断返回的响应的状态码是否为 HTTP_NOT_MODIFIED
,如果是的话直接返回缓存响应,这能提高效率。关于拦截器链的知识,我们在本篇就先介绍到这里了,在下一篇博客 okhttp源码解析(三) 中笔者会继续分析后两个拦截器 ConnectInterceptor
和 CallServerInterceptor
,有问题的话可以在下方的评论区给我留言,祝大家学习愉快!