源代码
GitHub源代码
本文目标
理解okhttp的拦截器各自的作用
拦截器
在发起请求的时候,当执行到execute()方法会调用
Response result = getResponseWithInterceptorChain();
这行代码,就是通过责任链模式的拦截器拿到响应的,具体代码如下
final class RealCall implements Call {
@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);
}
}
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
// 拦截器的一个集合
List<Interceptor> interceptors = new ArrayList<>();
// 客户端的所有自定义拦截器
interceptors.addAll(client.interceptors());// 自己的
// OKhttp 5 个拦截器 ,责任链设计模式,每一个拦截器只处理与他相关的部分
interceptors.add(retryAndFollowUpInterceptor);// 重试
interceptors.add(new BridgeInterceptor(client.cookieJar()));// 基础
interceptors.add(new CacheInterceptor(client.internalCache()));// 缓存
interceptors.add(new ConnectInterceptor(client));// 建立连接
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);
}
}
我们自己也可以实现 Interceptor 接口来自定义拦截器,okhttp会先将我们的拦截器添加到集合中,下面的5个拦截器都是okhttp自带的拦截器
1.RetryAndFollowUpInterceptor
处理重试的一个拦截器,会去处理一些异常,只要不是致命的异常就会重新发起一次请求(把Request给下级),如果是致命的异常就会抛给上一级;
会处理一些重定向等等,比如3XX 307,407就会从头部中获取新的路径,生成一个新的请求交给下一级(重新发送一次请求)
2.BridgeInterceptor
做一个简单的处理,设置一些通用的请求头,Content-Type,Connection,Content-Length,Cookie
做一些返回的处理,如果返回的数据被压缩了采用 ZipSource,保存Cookie
3.CacheInterceptor
在缓存可用的情况下,读取本地的缓存的数据,如果没有直接去服务器,如果有首先判断有没有缓存策略,然后判断有没有过期,如果没有过期直接拿缓存,如果过期了需要添加一些之前头部信息如:If-Modified-Since,这个时候后台有可能会给你返回 304 代表你是可以拿本地缓存,每次读取到新的响应后做一次缓存
4.ConnectInterceptor
findHealthyConnection()找一个连接,首先判断有没有健康的,没有就创建(建立Socket,握手连接),连接缓存
okHttp是基于原生的 Socket + okio (原生IO的封装)
封装 HttpCodec 里面封装了okio的 Source(输入) 和 Sink(输出),我们通过 HttpCodec 就可以操作 Socket的输入输出,我们就可以向服务器写数据和读取返回数据
5.CallServerInterceptor
写数据和读取数据
写头部信息,写body表达信息等等
连接的三个核心类
- RealConnection 建立连接的一个对象的封装
- ConnectionPool 保存了连接
- StreamAllocation 找一些链接,做一下封装
6.分发器(内部维护队列与线程池,完成请求调配)
对于同步请求,分发器只记录请求,⽤于判断IdleRunnable
是否需要执⾏。对于异步请求,向分发器中提交请求。
如何决定将请求放⼊ready还是running队列?
如果当前正在请求数不⼩于64放⼊ready;如果⼩于64,但是已经存在同⼀域名主机的请求5个也会放⼊ready!
从running移动到ready队列的条件是什么?
每个请求执⾏完成就会从running移除,同时进⾏第⼀步相同逻辑的判断,决定是否移动到ready!
分发器线程池的⼯作⾏为?
使⽤了缓存线程池(⽆等待,最⼤并发)+ 定义了线程⼯程(设置了线程名,设置不是守护线程)。
7.责任链模式
为请求创建了⼀个接收者对象的链,在处理请求的时候执⾏过滤(各司其职)。责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,⽆须关⼼请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。
getResponseWithInterceptorChain()方法都干了什么
1.把所有配置好的 Interceptor 放在⼀个 List ⾥,然后作为参数,创建⼀个 RealInterceptorChain 对象,并调⽤ chain.proceed(request) 来发起请求和获取响应。
2.在 RealInterceptorChain 中,多个 Interceptor 会依次调⽤⾃⼰的intercept() ⽅法。这个⽅法会做三件事:
1)、对请求进⾏预处理。
2)、预处理之后,重新调⽤ RealInterceptorChain.proceed() 把请求交给下⼀个 Interceptor。
3)、在下⼀个 Interceptor 处理完成并返回之后,拿到Response 进⾏后续处理,并返回给上⼀个拦截器。当然了,最后⼀个 Interceptor 的任务只有⼀个:做真正的⽹络请求并拿到响应。
8.Okhttp执行流程总结:
Okhttp的
所有的逻辑⼤部分集中在拦截器中,但是在进⼊拦截器之前还需要依靠分发器来调配请求任务。
首先newCall(Request)⽅法
会返回⼀个 RealCall
对象,它是Call 接⼝
的实现。
当调⽤ RealCall.execute()
的时候,RealCall.getResponseWithInterceptorChain()
会被调⽤,它会发起⽹络请求并拿到返回的响应,装进⼀个 Response
对象并作为返回值返回;
当调用RealCall.enqueue()
时候和RealCall.execute()
⼤同⼩异,区别在于 enqueue()
会使⽤ Dispatcher
的线程池来把请求发在后台线程进⾏,但实质上使⽤的同样也是 getResponseWithInterceptorChain()
⽅法。
getResponseWithInterceptorChain()
这行代码,就是通过责任链模式的拦截器拿到响应的,简单总结一下就是
整个OkHttp
功能的实现就在这五个默认的拦截器中,分别为: 重试
、桥接
、缓存
、连接
、请求服务
这5个拦截器。每⼀个拦截器负责的⼯作不⼀样,就好像⼯⼚流⽔线,最终经过这五道⼯序,就完成了最终的产品。但是与流⽔线不同的是,OkHttp
中的拦截器每次发起请求都会在交给下⼀个拦截器之前⼲⼀些事情,在获得了结果之后⼜⼲⼀些事情。整个过程在请求时是顺序的,⽽响应时则是逆序。当⽤户发起⼀个请求后,会由任务分发器Dispatcher
将请求包装并交给重试拦截器处理。
1、重试拦截器在交出(交给下⼀个拦截器)之前,负责判断⽤户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满⾜条件那么就会重新执⾏所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议
必备的请求头加⼊其中(如:Host,Content-Type,Connection,Content-Length
等等)并添加⼀些默认的⾏为(如:GZIP压缩
);在获得了结果后,调⽤保存cookie接⼝
并解析GZIP数据
。
3、缓存拦截器顾名思义,交出之前读取并判断是否使⽤缓存;获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建⼀个连接,并获得对应的socket流
;在获得结果后不进⾏额外的处理。
5、请求服务器拦截器进⾏真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。
在经过了这⼀系列的流程后,就完成了⼀次HTTP
请求!
OKhttp针对⽹络层有哪些优化?
1、多路复⽤(ConnectInterceptor):在 StreamAllocation.newStream
⽅法中获取 RealConnection
,在获取的时候我们会判断有没有之前的连接可以复⽤。
2、缓存(CacheInterceptor): okhttp
的缓存策略是,key
为请求 url的 MD5 值
,value
为响应。
3、压缩(bridgeInterceptor): response
通过 bridgeInterceptor
处理的时候会进⾏ gzip 压缩
,这样可以⼤⼤减⼩我们的 response
,但不是什么情况下都压缩,只有⽀持的时候才会进⾏压缩。
9.连接池相关:
OkHttp怎么实现连接池?
首先要思考一个问题是为什么需要连接池?
1.频繁的进⾏建⽴Sokcet连接
和断开Socket
是⾮常消耗⽹络资源和浪费时间的,所以HTTP
中的keepalive
连接对于降低延迟和提升速度有⾮常重要的作⽤。
2.它可以在⼀次TCP连接
中可以持续发送多份数据⽽不会断开连接。所以连接的多次使⽤,也就是复⽤就变得格外重要了,⽽复⽤连接就需要对连接进⾏管理,于是就有了连接池的概念。
3.OkHttp
中使⽤ConectionPool
实现连接池,默认⽀持5个并发KeepAlive
,默认链路⽣命为5分钟。
连接流程
1)⾸先,ConectionPool
中维护了⼀个双端队列Deque
,也就是两端都可以进出的队列,⽤来存储连接。
2)然后在ConnectInterceptor
,也就是负责建⽴连接的拦截器中,⾸先会找可⽤连接,也就是从连接池中去获取连接,具体的就是会调⽤到ConectionPool
的get⽅法
。也就是遍历了双端队列,如果连接有效,就会调⽤acquire⽅法
计数并返回这个连接。
3)如果没找到可⽤连接,就会创建新连接,并会把这个建⽴的连接加⼊到双端队列中,同时开始运⾏线程池中的线程,其实就是调⽤了ConectionPool
的put⽅法
。其实这个线程池中只有⼀个线程,是⽤来清理连接的,也就是cleanupRunnable
。
连接池如何清理
cleanupRunnable
的run⽅法
中会不停的调⽤cleanup⽅法
清理线程池,并返回下⼀次清理的时间间隔,然后进⼊wait
等待。它使⽤的回收算法类似于Java GC
中的标记清除算法
(标记不活跃的连接并删除:使⽤weakReference.get()
检测到空闲的socket
超过5个或者keepalive
时间⼤于5分钟将当前连接从连接池删除并关闭连接)。这样被线程池检测到被回收,这样就可以保持多个健康的keep-alive
连接。
总结一下连接池
主要就是管理双端队列``Deque<RealConnection>
,可以⽤的连接就直接⽤,然后定期清理连接,同时通过对StreamAllocation
的引⽤计数实现⾃动回收。
你从这个库中学到什么有价值的或者说可借鉴的设计思想?
使⽤责任链模式实现拦截器的分层设计,每⼀个拦截器对应⼀个功能,充分实现了功能解耦,易维护。