前言
看了我们这个系列文章的应该知道,前面我们多次提到拦截器链这个概念,然后说它是 OKHttp 当中非常重要的一个内容。那拦截器到底是个啥呢?官网给我们的解释是这样的:拦截器是OKHttp中提供的一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能。官网把拦截器分为两种:Application 拦截器、network拦截器,而 OKHttp 内部提供了一系列的拦截器给我们,这也是 OKHttp 拦截器的核心。
而拦截器链就是由一系列拦截器组成的,顺序调用的一个机制,简称拦截器链。另外很重要的一点是:拦截器链是不区分同步异步的,也就是说同步异步都会用到拦截器链。
拦截器链源码分析
在第二篇介绍异步请求时,我们提到过:在异步请求中,我们是通过拦截器链来获取 Response 的,也就是通过 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()));
//连接拦截器:建立可用的连接,是CallServerInterceptor的基本
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
//将http请求写进io流当中,并且从io流中读取response
interceptors.add(new CallServerInterceptor(forWebSocket));
//初始化 RealInterceptorChain
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
源码中前面部分主要提到了五个拦截器,分别是:
- RetryAndFollowUpInterceptor:重试和失败重定向拦截器
- BridgeInterceptor:桥接拦截器,处理一些必须的请求头信息的拦截器
- CacheInterceptor:缓存拦截器,用于处理缓存
- ConnectInterceptor:连接拦截器,建立可用的连接,是CallServerInterceptor的基本
- CallServerInterceptor:请求服务器拦截器将 http 请求写进 IO 流当中,并且从 IO 流中读取响应 Response
后半部分很重要了,先是初始化 RealInterceptorChain,然后调用 proceed() 方法,我们跟踪这个方法,进入到 RealInterceptorChain 的源码中,最终定位到 RealInterceptorChain 中的 proceed() 方法
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
//......省略部分代码,只显示核心代码
// Call the next interceptor in the chain.
//调用拦截器链中的下一个拦截器,注意 index + 1 参数
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;
}
从 RealInterceptorChain 的 proceed() 方法的核心代码中,我们可以看到。在这个方法中,主要就是新建了一个新的 RealInterceptorChain,只是在第五个 index 参数中,传入了 index + 1,确保调用到的是下一个拦截器,这也是 OKhttp 中拦截器链的关键所在。然后获取当前的拦截器,调用 Interceptor 的 intercept() 方法。我们可以查看上面五个拦截器的 intercept() 方法,可以发现,这五个拦截器的 intercept() 方法中,无一例外都调用了 chain.proceed() 方法,然后 proceed() 方法又会去调用下一个拦截器的 intercept()。Response也是由这条链依次向上返回。由此就形成了一个完整的链条。
从这里我们可以总结得到:拦截器通过 intercept() 调用拦截器链的 proceed() 方法,由此创建下一个拦截器链,然后又调用下一个拦截器的 intercept() 方法。从而形成一个调用链,依次获取 Response 返回。看下面的调用流程图
接下来我们就重点介绍一下 OKHttp 提供的五个拦截器
RetryAndFollowUpInterceptor 重试和重定向拦截器
从名字我们就能知道这个拦截器是用来做失败重连的,我们先看一下源码
@Override public Response intercept(Chain chain) throws IOException {
//......
//用于服务端数据传输的输入输出流,依次传到 ConnectInterceptor 用于 http 请求,本 intercepter 没有用到,核心点!!
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
this.streamAllocation = streamAllocation;
//......
while (true) {
Response response;
boolean releaseConnection = true;
try {
//调用拦截器链,执行下一个拦截器
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
//......
releaseConnection = false;
continue;
} catch (IOException e) {
//......
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
//......
//对重试次数进行判断,当重试次数大于20次时,释放掉streamAllocation
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
//......
}
}
这里只展示了部分核心代码,详细代码可查看源码。
前面我们提到,OKHttp 请求是通过 RealInterceptorChain 串联起所有的拦截器,然后依次往回返回 Response,只有所有拦截器正常返回,我们的请求才算成功。但是我们知道,网络在请求过程中是不稳定的,可能会存在不同程度的问题。这个时候 Response 返回码就不一定是200了,Response 也就不能用了。那我们这个拦截器就对这种情况进行了精密的部署,具体的逻辑在代码段的 while 循环中。我们只讲一下两处注释的地方。第一个注释位置:很明显,这里通过调用 chain.proceed() 方法去调用下一个拦截器,并获取 Response;第二次注释位置:这里对重试次数做了限制,避免无限制的重试,能跳出整个逻辑。
另外这段源码中,还创建了一个很重要的内容,那就是
StreamAllocation,它用于收集服务端数据传输时的输入输出流,在本拦截器中创建,但是会依次传到 ConnectInterceptor 用于 http 请求。
RetryAndFollowUpIntercepter 总结:
1. 创建StreamAllocation对象
2. 调用RealInterceptorChain.proceed(…)进行网络请求
3. 根据异常结果或者响应结果判断是否要进行重新请求
4. 调用下一个拦截器,对 Response 进行处理,返回给上一个拦截器
BridgeInterceptor 桥接拦截器
BridgeInterceptor 拦截器主要负责对头部信息进行添加(长度、编码方式、压缩等),看一下源码
//省略部分源码
//调用拦截器的 proceed() 方法,请求服务器,获取 Response。
Response networkResponse = chain.proceed(requestBuilder.build());
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
//判断 Response 是否进行了 Gzip 压缩,是则对 body 体进行解压。
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)));
}
前面我省略了一段源码,其实这一段主要就是对请求头进行了处理,大家可以去源码查看。我这里贴上的主要就是两段,一段就是前面一直提到的,调用拦截器的 proceed() 方法,执行下一个拦截器,获取 Response。另外一段就是判断 Response 是否进行了 Gzip 压缩,是则对 body 体进行解压。
BridgeInterceptor 总结:
1. 负责将用户构建的一个 Request 请求转化为能够进行网络访问的请求
2. 将这个符合网络请求的 Request 进行网络请求
3. 将网络请求回来的响应 Response 转化为用户可用的 Response
CacheInterceptor 缓存拦截器
首先我们在解析缓存拦截器源码前,我们先了解一下 OKHttp 缓存的使用方法。
public void cacheRequest() {
//设置缓存目录、大小
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(new File("cache"), 24 * 1024 * 1024)).build();
Request request = new Request.Builder().url("http://www.baidu.com").build();
Call call = client.newCall(request);
try {
call.execute();
} catch (IOException e) {
e.printStackTrace();
}
}
这里我们看到了一个新建的 Cache 类,传入缓存目录和缓存大小参数。跟踪进去查看源码发现,这个 Cache 类其实本质就是 DiskLruCache,也就是说,OKHttp 的缓存策略其实就是用 DiskLruCache 实现的。在分析 CacheInterceptor 前,我们先主要讲一下这个 Cache 类,主要就是 Cache 类的 put() 和 get() 两个方法。
@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;
}
//不缓存非 GET 方法的响应
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
// Entry 类:封装了各种属性,用于数据缓存
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
//先把 URL 经 MD5 加密,获取十六进制字符串,然后根据这个字符串创建 Editor
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
//把 Request 与 Response 的头部写入缓存
entry.writeTo(editor);
//返回 CacheRequestImpl,CacheRequestImpl 主要用于缓存 body 信息
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
put 方法总结:
- 判断是否是有效的缓存
- 判断是否是 GET 请求。注意:源码注释中提到,对非 GET 请求技术上能做到缓存,但是复杂性比较高,而且取得的收益很小,所以缓存只针对 GET 请求
- 使用 DiskLruCache.Editor 创建用于数据缓存的 Entry 类,缓存Request 和 Response 的头部信息,然后返回 CacheRequest 的实现类,用于缓存 body 信息。具体 body 缓存的实现位于 CacheIntercepor 中。
@Nullable Response get(Request request) {
//对 URL 进行MD5加密,获取加密后的 16进制字符串
String key = key(request.url());
//缓存快照
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//根据 key 值从缓存获取缓存快照
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
//赋值 Entry 对象
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//从 Entry 中获取 Response
Response response = entry.response(snapshot);
//匹配 Request 和 Response,如果不匹配,返回 null
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
get方法总结:
- 根据加密的 key 值,从缓存中获取缓存快照
- 创建 Entry 对象,从 Entry 中获取 Response
- 检测 Request 和 Response 是否匹配,不匹配返回 null,匹配返回 Response
在了解了 Cache 类的存、取方法后,我们再来分析 CacheInterceptor。先贴上源码:
@Override public Response intercept(Chain chain) throws IOException {
//获取缓存,可能为空
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//获取缓存策略类,CacheStrategy 内部维护着一个网络请求 Request 和一个缓存 Response,目的是:判断是使用网络还是缓存,还是两者都用
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//......
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
//当 networkRequest 和 cacheResponse 都为空时,构建一个错误码为504的 Response
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();
}
Response networkResponse = null;
try {
//获取 Response,交给下一个拦截器
networkResponse = chain.proceed(networkRequest);
} finally {
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) {
//当响应码为304,从缓存中获取数据
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();
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) {
//http 头部是否有响应体,并且缓存策略可以被缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.写入 Response 到缓存中
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//判断 Request 方法是否是有效的缓存,是的话,移除这个缓存。
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
详情查看上面源码的注释。从源码中我们可以发现,OKHttp 的缓存主要是根据 CacheStrategy 这个缓存策略类来处理的,它决定了是使用网络还是缓存,又或是两者都用。源码最后可以看到,当有响应体且可以缓存时,缓存当前数据。然后还判断了,当请求是无效缓存时,要清除这个缓存。CacheInterceptor 缓存拦截器的整个逻辑就是这样了,重点在 CacheStrategy 这个缓存策略类。
接下来还有两个拦截器没有分析,分别是 ConnectInterceptor 和 CallServerInterceptor 拦截器,因为篇幅过长,所以这两个拦截器将在下一篇文章中进行讲解。