今天我们来看看OkHttp
中另外一个大头--Interceptor。Okhttp
真正的网络请求都在这个拦截器链中执行的,所以分析OkHttp
的拦截器链是非常有必要的。
1. 概述
在正式分析拦截器链之前,我们先来每一个拦截器有一个大概的认识,同时对拦截器链的调用过程有一个宏观上的理解。
在OkHttp
中,给我们分配了5种拦截器,它们分别是:RetryAndFollowUpInterceptor
、BridgeInterceptor
、CacheInterceptor
、ConnectInterceptor
、CallServerInterceptor
。这里先对每一种拦截器的作用做一个总结。
类名 | 作用 |
---|---|
RetryAndFollowUpInterceptor | 主要负责网络请求的失败重连 |
BridgeInterceptor | 主要是将用户的Request转换为能够真正进行网络请求的Request,负责添加一些相应头;其次就是将服务器返回的Response解析成为用户用够真正使用的Response,负责GZip解压之类的 |
CacheInterceptor | 主要负责网络请求的Response缓存管理 |
ConnectInterceptor | 主要负责打开与服务器的连接,正式进行网络请求 |
CallServerInterceptor | 主要负责往网络数据流写入数据,同时接收服务器返回的数据 |
在这里,再对整个拦截器链做一个小小的总结。
拦截器链的调用是从RealCall
的getResponseWithInterceptorChain
方法开始的,在这个方法里面,会创建一个RealInterceptorChain
对象,然后调用了RealInterceptorChain
的proceed
方法,进而,在RealInterceptorChain
的proceed
方法里面会调用第一个拦截器的intercept
方法。而在拦截器的intercept
方法里面,会再创建一个RealInterceptorChain
对象,然后调用proceed
方法,而此时,在proceed
方法里面,调用就是第二个拦截器的intercept
方法。如下图:
2. RetryAndFollowUpInterceptor
在getResponseWithInterceptorChain
方法里面,除了我们自定义的拦截器之外,第一个调用的就是RetryAndFollowUpInterceptor
,所以,我们来看看RetryAndFollowUpInterceptor
究竟为我们做了那些事情。
RetryAndFollowUpInterceptor
主要是负责网络请求的失败重连。
@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) {
//...代码省略
}
}
从上面的代码中,我们可以看到,创建了StreamAllocation
对象,这个类主要负责网络数据流的管理。在RetryAndFollowUpInterceptor
类里面只是对StreamAllocation
对象进行了初始化,并没有使用它来进行网络请求,真正使用它的地方是在ConnectInterceptor
里面。
其次,就是在while
循环里面,这里就不详细的展开了,只是对一个进行简单的看看:
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
这句话非常的简单,就是如果当前重试的次数超过最大次数,就是结束当前的重连操作,并且抛出异常。
3. BridgeInterceptor
在前面的总结表中,已经对BridgeInterceptor
做了一个小小的总结。
BridgeInterceptor
在调用RealInterceptorChain
的 proceed
方法之前,会向RequestBuilder
里面添加很多的信息,包括Content-Type
、Content-Length
等等。这些都是网络请求的必要信息,这里我们就不需要过多的关注。这个在后面讲解ConnectionPool
时,会详细的解释
但是,有一个参数需要的特别注意,那就是Connection
在不为null时,会设置为Keep-Alive
,这个参数表示,一些TCP连接是否保持连接,如果保持连接,就可以达到复用的效果。
当调用proceed
返回结果时,此时还需要一步操作,那就是如果Response就是经过GZip压缩的,就需要解压。如下代码:
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)));
}
当然这个服务端是否返回GZip压缩的数据,还要取决于在添加Request的响应头时,是否告诉服务端,客户端支持Gzip压缩;如果客户端支持,那么服务端就会返回Gzip压缩过的数据。
4. CacheInterceptor
CacheInterceptor
的职责我们从这个类的名字就可以知道,它主要负责Response的缓存管理,包括哪些请求可以从缓存中取,哪些数据需要重新进行网络请求;哪些数据可以缓存,哪些数据不能被缓存等等。这些操作都是由CacheInterceptor
来管理的。
(1).缓存原理
在正式了解CacheInterceptor
之前,我们还是先来看看OkHttp怎么使用Cache
,同时了解OkHttp的缓存原理。
private final OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
.readTimeout(50, TimeUnit.SECONDS)
.cache(new Cache(new File("pby123"),1000))
.build();
如果我们需要将某些Response
缓存,可以直接在创建OkHttpClient
的对象时,调用cache
方法来进行配置。
CacheInterceptor
在使用缓存时,是通过调用Cache
的get
方法和put
方法来进行数据的获取和缓存。我们分别来看看。
(2).put方法
在Cache
类里面,我们需要重点关注的是put
方法和get
方法,因为CacheInterceptor
是通过调用这个方法来进行数据缓存的相关操作的。我们先来看看put
方法:
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
我将put
方法分为两个大步骤。
首先是判断当前的Response是否支持缓存,比如当前的请求方式是Post,就不支持缓存:
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;
}
如果当前的Response
支持缓存,那么就可以进行缓存的操作,这就是第二步。首先是,将当前的Response
使用Entry
包装起来,然后将创建DiskLruCache.Editor
的对象,最后就是使用DiskLruCache.Editor
对象来对数据进行写入。
总体上来说,还是比较简单的。不过在在这里我们发现,OkHttp是DiskLruCache
来实现缓存的操作,这一点需要特别的注意。
(3).get方法
说了put
方法,get
方法就更加的简单了。put
方法是缓存数据,那么get
方法就是获取缓存数据。我们还是简单的看看:
@Nullable Response get(Request request) {
String key = key(request.url());
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;
}
get
方法总体来说比较,这里就不做过多的解释了。
OkHttp的缓存原理也没有传说中那么高大上,就是一些普通的操作。在简单的了解缓存原理之后,我们来看看CacheInterceptor
。
(4). CacheInterceptor
CacheInterceptor
的intercept
方法比较长,这里就不完全贴出来,这里以分段的形式解释。
我觉得,可以将CacheInterceptor
的intercept
过程分为三个步骤。
1.从缓存中获取数据,如果缓存数据有效的,之前返回,不进行网络请求。
2.调用RealInterceptorChain
的proceed
方法,进行网络请求,获取数据。
3.如果请求的数据支持缓存的话,那么缓存起来。
我们一个一个的来看看。首先来看获取缓存数据这部分。
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();
}
在整个过程中,思路还是比较清晰的。首先是调用Cache
的get
方法获取相应的缓存数据;然后就是创建一个缓存策略对象,这个缓存策略对象在后面有很大的作用,后面对它单独的讲解,这里就先对它有一个了解吧。
接下就有四个判断,需要重点解释一下。
if (cache != null) {
cache.trackResponse(strategy);
}
上面代码表示的意思是,如果当前的Cache
不为空,那么就追踪一下当前的缓存数据,主要是更新一下,获取缓存的次数,以及缓存命中率。
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++;
}
}
然后就是第二个判断。
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
上面代码的意思表示,如果从缓存里面获取的数据不为空,但是缓存策略里面的Response
为空,就表示当前从缓存中获取数据无效的,所以调用closeQuietly
方法关闭缓存相应的数据。
我们再来看看第三个判断。
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();
}
如果缓存策略里面的Request
和Response
都为空,表示当前网络不可用,并且没有缓存数据,所以就是返回504。
最后我们来看看判断。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
这里就非常简单了,如果当前的网络请求不可用,并且缓存数据不为空,那么就返回缓存的数据。
第二个过程就是调用RealInterceptorChain
的proceed
方法,这里就不详细的解释。我们直接来看看第三个过程。
// 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.
}
}
}
缓存数据的过程也是非常的简单的。
首先返回的数据如果是HTTP_NOT_MODIFIED
并且缓存数据不为空,那么更新一下缓存数据,否则的话,就关闭缓存的数据。
其次,就是返回的数据支持缓存,那么就是调用cacheWritingResponse
方法来缓存。
如上就是整个RealInterceptorChain
的缓存过程。接下来,我们来分析一下整个过程中贯穿的缓存策略类CacheStrategy
。
(5).CacheStrategy
我们直接来看CacheStrategy.Factory
的get
方法。看看这个方法到底做了什么。
不过在看get
方法之前,我们必须得有认知,那就是CacheStrategy
里面的networkRequest
和cacheResponse
到底代表着什么。
- networkRequest:表示一个网络请求,如果这个对象不为空的话,那么在
CacheStrategy
里面肯定会进行网络请求,至于最后是选择缓存数据还是请求回来的数据,得看具体的情况。- cacheResponse:表示缓存的
Response
,如果当前的网络不可用,也就是networkRequest
为空,那么会直接返回缓存的数据;如果networkRequest
不为空,那么就得跟请求回来的数据比较,具体的比较,可以参考上面的第三个过程。
现在,我们来正式看看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;
}
其实get
方法也没有做什么,核心操作还是在getCandidate
里面。由于getCandidate
方法过于长,这里直接贴出代码,在代码中解释。
private CacheStrategy getCandidate() {
// 没有缓存,返回一个request和一个为null的cacheResponse
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 如果当前是Https的请求,并且没有握手
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
//如果当前的cacheResponse不支持缓存
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();
// 如果当前的Response不可以被改变
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}
// 省略代码
}
其实,到最后来看,缓存策略CacheStrategy
并不是那么的可怕,还是比较通俗易懂的。
5. ConnectInterceptor
现在,我们来看看ConnectInterceptor
这个类。我们先来看看ConnectInterceptor
的proceed
方法:
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
整个过程还是简单的,不过有几个地方还是需要我们注意的。
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
还记得在RetryAndFollowUpInterceptor
里面,在那里创建了StreamAllocation
对象,但是没有用。当时,我也说了,StreamAllocation
对象真正的使用实在ConnectInterceptor
里面,这也正证实了当时的描述。
然而这一句有什么用呢?通过调用StreamAllocation
的newStream
方法,返回了一个HttpCodec
对象。这个HttpCodec
对象的作用是对网络请求进行编码和对网络请求回来的数据进行解码,这些操作都是HttpCodec
类给我们分装好的。
然后就是调用StreamAllocation
的connection
方法获取RealConnection
对象。
最后就调用RealInterceptorChain
的proceed
方法。这一步是常规操作。
看到这里来,是不是感觉心里面有一点失落?感觉ConnectInterceptor
在intercept
方法里面并没有做什么事。
其实真正的操作并没有在intercept
方法里面,而是在StreamAllocation
的newStream
方法里面。我们来看看newStream
方法。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
其实newStream
方法也是非常的简单,重点还是在findHealthyConnection
方法,我们来看看。
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// If this is a brand new connection, we can skip the extensive health checks.
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
findHealthyConnection
方法的作用是找到一个健康的连接,在findHealthyConnection
方法里面,我们可以看到,不断调用findConnection
方法来找到一个连接,然后判断当前这个连接是否是健康,如果是健康的,那么就返回,否则就重新寻找。
这里的健康是一个宏观的概念,那什么表示不健康呢?如果一个连接没有关闭或者相关的流没有关闭都表示该连接是不健康的。
我们再来看看findConnection
方法,由于findConnection
方法过长,这里就不全部贴出来了。简单的解释一下这个方法整个执行流程。
findConnection
方法主要分为两步:
- 找到一个可以使用的
Connection
。- 调用
Connection
的connect
方法进行连接。
先来看看第一步。
// Attempt to use an already-allocated connection. We need to be careful here because our
// already-allocated connection may have been restricted from creating new streams.
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
if (result == null) {
// Attempt to get a connection from the pool.
Internal.instance.get(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
寻找一个可以使用的Connection
过程看上去还是比较简单的。总体上来说,先尝试复用当前StreamAllocation
的connection
,如果可以复用的话,直接拿来用;否则的话,就去连接池 connectionPool
里面去取得一个可以使用的连接。
第二步操作就是调用Connection
的connect
方法。这连接方法里面就涉及到非常多的东西,包括连接方式(socket连接还是隧道连接)等等。这里不进行展开了。
最后在findConnection
方法里面,我们可以又把获得的连接放回了连接池中去了:
synchronized (connectionPool) {
reportedAcquired = true;
// Pool the connection.
Internal.instance.put(connectionPool, result);
// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
6. CallServerInterceptor
CallServerInterceptor
是OkHttp
中的5大拦截器最后一个拦截器。CallServerInterceptor
拦截器的作用在上面做了一个简单的介绍,在这里将结合代码来说明CallServerInterceptor
到底做了那些事情。
CallServerInterceptor
的执行过程,我们可以分为两步
- 往网络请求中写入数据,包括写入头部数据和body数据。
- 接收服务器返回的数据。
具体的细节这里就不在详细的解释了,都是一些常规操作。
7. 总结
Interceptor
是Okhttp
里面比较核心的东西,同时也是比较复杂的东西,这里对拦截器做一个简单的总结。
- 在
OKhttp
里面,5大拦截器构成一个链,从RealCall
的getResponseWithInterceptorChain
方法开始,5大拦截器开始执行。- 5大拦截是通过
RealInterceptorChain
连接起来的,它们之间调用过程一定熟悉。- 5大拦截器,其中每个拦截的作用都需要熟悉。
网友评论