一.五大拦截器总体介绍

在前面分发器的介绍中,可以看到通过​​getResponseWithInterceptorChain​​这一个方法,就可以获得响应。这个方法里面是这样的

OkHttp源码解析(二)五大拦截器_android


除了两个用户可以自己添加的拦截器之外,剩下的五个是默认的拦截器。他们之间是用责任链模式连接在一起

1.责任链模式

概念:责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以职 责链将请求的发送者和请求的处理者解耦了

比如点个外卖,它的流程是这样的

OkHttp源码解析(二)五大拦截器_拦截器_02


我不必关心内部是具体怎么给我做的饭,美团外卖只管提供平台和送餐,饭店只管接单和派单,厨师只管接收菜和做菜。他们之间是一个U型结构,类似于Android的事件分发。这五大拦截器之间也是责任链模式,具体可用一张图表示

OkHttp源码解析(二)五大拦截器_android_03

2.五大拦截器的责任链模式抽取

我们建立这几个类和接口

OkHttp源码解析(二)五大拦截器_apache_04


上面五个是拦截器,​​Chain​​​是链条类,负责串联这几个拦截器,​​Interceptor​​​是接口,​​MyClass​​​是程序入口。我们进入​​MyClass​

OkHttp源码解析(二)五大拦截器_apache_05


这里首先创建了五个拦截器,然后通过​​Chain​​将五大拦截器串起来,​​index​​设为0,然后调用​​chain​​的​​processd​​方法

OkHttp源码解析(二)五大拦截器_java_06


看下第一个拦截器的​​intercept​​方法

OkHttp源码解析(二)五大拦截器_拦截器_07


然后调用​​chain​​(索引值从1开始,也就是从第二个拦截器开始)的​​processd​​方法。再以此类推,直到调用了第五个拦截器的​​intercept​​方法

OkHttp源码解析(二)五大拦截器_android_08


然后依次返回,一直到第一个。

也就是说先调用​​chain​​的​​processd​​方法,找到第一个拦截器,然后调用其​​intercept​​方法,然后再调用​​chain​​的​​processd​​方法,找到第二个拦截器,然后调用其​​intercept​​方法,然后再调用​​chain​​的​​processd​​方法,以此类推,直到找到第五个拦截器,调用其​​intercept​​方法,执行完了之后将结果依次返回到第一个拦截器。

3.各个拦截器的功能

重试和重定向拦截器

它在交出(交给下一个拦截器)之前,负责判断是否需要重新发起请求(重试);在获得了结果之后 ,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。 (它是最早接收​​Request​​​的,也是最晚接收​​Response​​的)

OkHttp源码解析(二)五大拦截器_缓存_09


返回码是3xx的,就需要重定向了

桥接拦截器

在交出之前,负责将HTTP协议必备的请求头,加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

缓存拦截器

顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。

连接拦截器

在交出之前,负责找到或者新建一个连接,并获得对应的​​socket​​流;在获得结果后不进行额外的处理。

请求服务器拦截器

进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

二.RetryAndFollowUpInterceptor

重试和重定向拦截器。它对​​Request​​​没有做什么特殊的操作,而是把重心放在了​​Response​​上面。直接看源码

OkHttp源码解析(二)五大拦截器_apache_10


可以看到它直接把​​request​​传进去的。

这里有几个值得注意的点

①整个重试和重定向过程是在一个​​while​​循环里面

②通过​​try catch​​​来实现重试,具体是​​catch​​​里面的​​continue​

OkHttp源码解析(二)五大拦截器_拦截器_11


也就是请求异常的时候,会进入​​catch​​里面,如果​​recover​​方法返回​​true​​,那就可以重试。下面就进入这个​​recover​​方法看一看

1.recover判断是否可以重试

OkHttp源码解析(二)五大拦截器_java_12


不允许重试的情况有四种

①通过其建造者模式,创建OkHttpClient的时候,给他配置了不可重试。

在其​​Builder​​​类里面,有这么一个方法,传入​​false​​,就设置了不可重试

OkHttp源码解析(二)五大拦截器_拦截器_13


当然,如果不设置,默认情况下,是​​true​​,即可重试

OkHttp源码解析(二)五大拦截器_拦截器_14


②对某一次请求设置不可重试。(用的少)

上面那种情况是全局的,这个是某一次的。具体方法是自定义一个请求体,然后让其实现标识接口​​UnrepeatableRequestBody​​。

如图

OkHttp源码解析(二)五大拦截器_缓存_15


isRecoverable方法返回false,不可重试

我们看下​​isRecoverable​​方法

OkHttp源码解析(二)五大拦截器_android_16


如果是协议异常,就不可重试

比如在​​CallServerInterceptor​​的这里

OkHttp源码解析(二)五大拦截器_android_17


204和205的意思是响应体中没有内容,而后面的​​contentLength​​大于0又说响应体中有内容,所以协议冲突了,抛出异常。一般是服务器的原因

OkHttp源码解析(二)五大拦截器_缓存_18


如果是请求时间过长,那么可以重试(比如发生网络波动了)

如果是证书不正确,或者不匹配,都不能重试

④有没有更多的路由

比如有A代理和B代理,A代理不行,可以试试B代理,两者都不行,就是没有路由了,就不能重试了所以,总结下重试的逻辑

如果OkHttpClient允许重试,​isRecoverable​方法返回true,且拥有更多的路线,才能进行重试。如图

OkHttp源码解析(二)五大拦截器_拦截器_19


所以重试的条件是很苛刻的。

2.重定向

假如成功获得响应,那么走出​​try catch​​,走下面,如图

OkHttp源码解析(二)五大拦截器_apache_20


如果返回的响应码是需要重定向的响应码,如301,302,则​​followUpRequest​​​方法会解析响应头里面的​​location​​​字段,并将​​location​​​里面的​​url​​​组建成一个新的​​request​​​,返回,也就是让​​followUp​​​接收。如果​​followUp​​​为​​null​​​,就是没有​​location​​​,那就是不需要重定向。当然重定向的次数有限制,最多为20次,即​​MAX_FOLLOW_UPS​​除了30x的响应码,需要重定向之外,其他的有一些响应码需要特殊处理。如图

OkHttp源码解析(二)五大拦截器_java_21

三.BridgeInterceptor

桥接拦截器,

它对​​Request​​的操作,就是帮我们添加各种请求头。如图

OkHttp源码解析(二)五大拦截器_android_22


补全之后就将请求交给下一拦截器进行处理。得到响应后,主要干两个事情

1、保存​​cookie​​​ 2、如果使用​​gzip​​返回的数据,则使用 ​​GzipSource​​ 包装便于解析。

四.CacheInterceptor

缓存拦截器,只有GET请求可以使用,POST那些不行。且如果要使用它的缓存,就要在创建​​OkHttpClient​​的时候进行手动配置

OkHttp源码解析(二)五大拦截器_java_23


当然,也可以指定某一次请求不使用缓存

OkHttp源码解析(二)五大拦截器_拦截器_24

他有一个核心类,就是缓存策略类即​​CacheStrategy​​​。它会经过很复杂的逻辑判断,得到两个值,分别是​​networkRequest​​​和​​cacheResponse​​​。根据两者是否为​​null​​的组合,来判断到底是进行网络请求还是使用缓存。具体规则为

OkHttp源码解析(二)五大拦截器_apache_25

下面,具体看一下是如何得到最终的​​CacheStrategy​​的,也就是这一行代码干了什么事

OkHttp源码解析(二)五大拦截器_android_26


进入​​get​​方法

OkHttp源码解析(二)五大拦截器_apache_27


进入​​getCandidate​​方法

里面就是具体怎么得到的​​CacheStrategy​​。逻辑很复杂,有些我也没有学习,这里找到了一些带注释的代码,供大家参考

private CacheStrategy getCandidate() {
// No cached response.
//todo 1、没有缓存,进行网络请求
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}

//todo okhttp会保存ssl握手信息 Handshake ,如果这次发起了https请求,但是缓存的响应中没有握手信息,发起网络请求

//todo 2、https请求,但是没有握手信息,进行网络请求
//Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}

//todo 3、主要是通过响应码以及头部缓存控制字段判断响应能不能缓存,不能缓存那就进行网络请求
//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();
//todo 4、如果 请求包含:CacheControl:no-cache 需要与服务器验证缓存有效性
// 或者请求头包含 If-Modified-Since:时间 值为lastModified或者data 如果服务器没有在该头部指定的时间之后修改了请求的数据,服务器返回304(无修改)
// 或者请求头包含 If-None-Match:值就是Etag(资源标记)服务器将其与存在服务端的Etag值进行比较;如果匹配,返回304
// 请求头中只要存在三者中任意一个,进行网络请求
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}

//todo 5、如果缓存响应中存在 Cache-Control:immutable 响应内容将一直不会改变,可以使用缓存
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
return new CacheStrategy(null, cacheResponse);
}

//todo 6、根据 缓存响应的 控制缓存的响应头 判断是否允许使用缓存
// 6.1、获得缓存的响应从创建到现在的时间
long ageMillis = cacheResponseAge();
//todo
// 6.2、获取这个响应有效缓存的时长
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {
//todo 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
freshMillis = Math.min(freshMillis,
SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
//todo
// 6.3 请求包含 Cache-Control:min-fresh=[秒] 能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}

//todo
// 6.4
// 6.4.1、Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
// 6.4.2、Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长 如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
// 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

//todo
// 6.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以使用的时间
// 允许使用缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
//todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
//todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
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.
//todo 7、缓存过期了
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.
}
//todo 如果设置了 If-None-Match/If-Modified-Since 服务器是可能返回304(无修改)的,则使用缓存的响应体
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);
}

有一个比较重要的请求头/响应头,就是​​Cache-Control​​​​Cache-Control​​ 可以在请求头存在,也能在响应头存在,对应的​​value​​可以设置多种组合:

1.max-age=[秒] :资源最大有效时间;
2.public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源; 3.
3.private :表明该资源只能被单个用户缓存,默认是private。
4.no-store :资源不允许被缓存
3.no-cache :(请求)不使用缓存,就是这一次不用,并不代表没有缓存
4.immutable :(响应)资源不会改变
5.min-fresh=[秒] :(请求)缓存最小新鲜度(用户认为这个缓存有效的时长) 8. must-revalidate :(响应)不允许使用过期缓存
6.max-stale=[秒] :(请求)缓存过期后多久内仍然有效

举例:假设存在max-age=100,min-fresh=20。这代表了用户认为这个缓存的响应,从服务器创建响应 到 能够缓
存使用的时间为100-20=80s。但是如果max-stale=100。这代表了缓存有效时间80s过后,仍然允许使用
100s,可以看成缓存有效时长为180s。

具体流程是这样的

OkHttp源码解析(二)五大拦截器_apache_28


总结一下

如果想使用缓存,首先用户要配置缓存,然后查看是否存在缓存,且要保持有握手信息。除此之外,看是否允许使用缓存,若允许,则看这次请求是否需要使用缓存,如果需要,则看返回体中是否有​​Cache-Control:immutable​​​,若有,则说明缓存的资源没变,则再看看资源是否有效,也就是是否过期了。如果没过期,才能用缓存。使用缓存的话,就不需要使用下面的拦截器了,直接返回​​response​

五.ConnectInterceptor

连接拦截器。说白了就是获取​​socket​​​连接。该连接有可能是从​​socket​​连接池里面获取的

1.连接池ConnectionPool

首先看它的构造方法

OkHttp源码解析(二)五大拦截器_拦截器_29


第一个是最大闲置连接数,也就是说它最多维护多少个闲置连接,类似于线程池的核心线程数。后面就是其存活时间和单位了。往连接池中添加连接,使用的是put方法

OkHttp源码解析(二)五大拦截器_apache_30


如果是第一次调用​​put​​​方法,就会启动​​cleanupRunnable​​,即专门清理闲置连接的一个线程

OkHttp源码解析(二)五大拦截器_缓存_31


追踪里面的​​cleanUp​​方法

逻辑大概是这样:首先遍历所有的连接,得到闲置连接数和闲置了最长时间的连接。遍历完成后,如果闲置了最长时间的连接超过了5分钟(或者你自定义的其他时长),或者闲置连接数超过了5(或者你自定义的其他数目),则把这个闲置了最长时间的连接从连接池里面清除掉。具体逻辑在下面

long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;

// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();

//todo 检查连接是否正在被使用
//If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}
//todo 否则记录闲置连接数
idleConnectionCount++;

// If the connection is ready to be evicted, we're done.
//TODO 获得这个连接已经闲置多久
// 执行完遍历,获得闲置了最久的连接
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//todo 超过了保活时间(5分钟) 或者池内闲置连接数量超过了(5个) 马上移除闲置了最久的连接,然后返回0,表示不等待,马上再次检查清理
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it
// below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
//todo 池内存在闲置连接,就等待 保活时间(5分钟)-最长闲置时间 =还能闲置多久 再检查
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we
// run again.
//todo 有使用中的连接,就等 5分钟 再检查
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
//todo 都不满足,可能池内没任何连接,直接停止清理(put后会再次启动)
cleanupRunning = false;
return -1;
}
}

closeQuietly(longestIdleConnection.socket());

// Cleanup again immediately.
return 0;
}

get方法

首先会遍历连接池里面的各个连接,判断ip端口等是否与要请求的相同,而且该连接是否空闲。都满足的时候,就可以复用。

比如进入​​isEligible​​看一看

OkHttp源码解析(二)五大拦截器_缓存_32

public boolean isEligible(Address address, @Nullable Route route) {
// If this connection is not accepting new streams, we're done.
//todo 实际上就是在使用(对于http1.1)就不能复用
if (allocations.size() >= allocationLimit || noNewStreams) return false;


// If the non-host fields of the address don't overlap, we're done.
//todo 如果地址不同,不能复用。地址包括了配置的dns、代理、证书以及端口等等 (域名还没判断,所以下面马上判断域名)
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

// If the host exactly matches, we're done: this connection can carry the address.
//todo 都相同,那就可以复用了
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}

。。。。。
return true; // The caller's address can be carried by this connection.
}

六.CallServerInterceptor

请求服务拦截器,这个拦截器就是完成HTTP协议报文的封装与解析。

有一个需要注意的点,就是需要上传大容量请求体的时候。可以用一个

OkHttp源码解析(二)五大拦截器_apache_33


请求头,代表了先询问服务器是否愿意接收发送请求体数据。

OkHttp是这样处理的

如果服务器允许则返回100,客户端继续发送请求体; 如果服务器不允许则直接返回给用户。 同时服务器也可能会忽略此请求头,一直无法读取应答,此时抛出超时异常。