整体流程
使用OKhttp进行网络请求的的起始点是构造OkHttpClient与Request的实例,接着构建一个call并调用同步或者异步执行的方法。在OKhttp的内部会接着执行应用拦截器、重定向拦截器、桥接拦截器、缓存拦截器、连接拦截器、网络拦截器、执行拦截器,最后就是等待服务端的请求返回。
调度器
在okhttp3/Dispatcher.kt这个类中,调度器作用是维护任务队列与线程池并完成请求调配,内部有如下三个队列,内部的核心逻辑也是主要围绕着这三个队列。
同步任务队列:runningSyncCalls
异步任务等待队列:readyAsyncCalls
异步任务队列:runningAsyncCalls
同步任务会被add到runningSyncCalls中,接着便开始调getResponseWithInterceptorChain方法同步获取请求结果,最后在finish执行完成的任务,clean掉runningSyncCalls。对于同步任务来说,任务在当前线程同步执行,调度器没有实质作用,只是进行记录而已。
异步任务会先被添加到readyAsyncCalls中,接着遍历readyAsyncCalls,满足条件后则将call加入到runningAsyncCalls中执行。最终调用executorService.execute,回调AsyncCall的RUN方法,在run方法里执行getResponseWithInterceptorChain,将请求结果Callback回调回去,回调完成后清理异步任务队列。
promoteAndExecute是Dispatcher中一个比较关键的方法,主要有三个作用:
- readyAsyncCalls任务移交runningAsyncCalls;
- 线程池执行任务;
- 返回是否是执行任务状态。 promoteAndExecute关键代码如下:
...
synchronized(this) {
val i = readyAsyncCalls.iterator()
while (i.hasNext()) {
val asyncCall = i.next()
if (runningAsyncCalls.size >= this.maxRequests) break // 队列中超过阈值(64)终止
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // 超过并发执行阈值(5)终止
i.remove()
asyncCall.callsPerHost.incrementAndGet() // 并发执行数自增
executableCalls.add(asyncCall)
runningAsyncCalls.add(asyncCall) // 切换队列
}
isRunning = runningCallsCount() > 0
}
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
// 线程池执行任务
asyncCall.executeOn(executorService)
}
...
两个重点
任务队列内部是用链表实现的双端队列的原因是与readyAsyncCalls向runningAsyncCalls转换有关,当执行完一个请求或调用enqueue方法入队新的请求时,会对readyAsyncCalls进行一次遍历,将那些符合条件的等待请求转移到runningAsyncCalls队列中并交给线程池执行。尽管二者都能完成这项任务,但是由于链表的数据结构致使元素离散的分布在内存的各个位置,CPU缓存无法带来太多的便利,另外在垃圾回收时,使用数组结构的效率要优于链表。
OKHttp线程池阻塞队列用的SynchronousQueue,它的特点是不存储数据,当添加一个元素时,必须等待一个消费线程取出它,否则一直阻塞,如果当前有空闲线程则直接在这个空闲线程执行,如果没有则新启动一个线程执行任务。通常用于需要快速响应任务的场景,在网络请求要求低延迟的大背景下比较合适。
拦截器
OkHttp将整个请求的复杂逻辑切成了一个一个的独立模块并命名为拦截器(Interceptor),通过责任链的设计模式串联到了一起,最终完成请求获取响应结果。
| 名称 | 作用 | |
|---|---|---|
| Interceptors | 应用拦截器拿到原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。 | |
| RetryAndFollowUpInterceptor | 重试与重定向拦截器,处理错误重试和重定向。 | |
| BridgeInterceptor | 桥接拦截器主要工作是为请求添加cookie、设置其他报头,如User-Agent,Host,Keep-alive等。设置gzip压缩,并在接收到内容后进行解压。 | |
| CacheInterceptor | 缓存拦截器主要是使用缓存与设置缓存 | |
| connectInterceptor | 连接拦截器内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。 | |
| NetworkInterceptors | 网络拦截器属于用户自定义拦截器,通常用于监控网络层的数据传输。 | |
| CalllServerInterceptor | 请求拦截器,在前置准备工作完成后,真正发起了网络请求。 |
RetryAndFollowUpInterceptor
server收到一个client请求后,发现请求的这个资源实际放在另一个位置,于是server在返回的响应头的Location字段中写入那个请求资源的正确的URL,并设置reponse的状态码为30x,端上收到重定向的响应后,会在重定向拦截器里用新的URL重新发起请求。假如一个请求在 RetryAndFollowUpInterceptor 这个拦截器内部重试或者重定向了 N 次,那么其内部嵌套的所有拦截器也会被调用N次。
重拾与重定向拦截器的逻辑如下:
CacheInterceptor
HTTP 304 未改变说明无需再次传输请求的内容,也就是说可以使用缓存的内容。
OKhttp默认仅支持GET请求缓存,实际上也只有GET请求的缓存比较有实际意义。
缓存拦截器的执行流程如下:
ConnectInterceptor
从源码上看ConnectInterceptor这个类十分简洁,核心的实现全部都在下边这行代码,方法内部实现了DNS解析、Sokect建连等逻辑
realChain.call.initExchange(chain)
主要的时序如下:
连接复用
两个关键判断
- 当前连接是否可用?
(1) 当前RealCall的已经建立的的连接的host、port是否与正在发起链接的一致。
(2) 已存在的连接可以复用,如果这个连接出现一些异常、在链接池中被移除等就不能直接复用了。
- 当前链接池中的连接是否可用?
(1) 当前的连接的最大并发数不能达到上限,否则不能复用
(2) 两个连接的address的Host、port不相同,不能复用
(3) 1、2通过后,url的host相同则可以复用
(4) 如果3中url的host不相同,可以通过合并连接实现复用
(5) 但首先这个连接需要是HTTP/2
(6) 不能是代理
(7) IP的address要相同
(8) 这个连接的服务器证书必须覆盖新的主机
(9) 证书将必须匹配主机
总结
本文从OKhttp的请求出发,讲叙了OKhttp中调度器和拦截器这两个重点,通过流程图分析了RetryAndFollowUpInterceptor、CacheInterceptor,通过connectInterceptor执行的时序图、connectInterceptor链接复用流程图剖析了整体结构与连接复用的原理。