前言
继Glide篇和RxJava2篇之后的又一篇源码分析,和之前一样先列出要点,然后一步一步分析~
- okhttp完整请求过程(Client、Request、Call、Interceptor、Response)
- okhttp拦截器分析(RetryAndFollowUp、Bridge、Cache、Connect、CallServer)
- okhttp缓存相关(CacheStrategy、RealConnectPool、Deque
)
OkHttp完整请求过程
一次完整的okhttp请求过程大致可以概括为:
- 构建
OkHttpClient
- 构建
Request
- 同步或异步发出请求,并经过
Interceptors
的处理 - 得到
Response
下面逐步分析,首先创建OkHttpClient
:
|
|
OkHttpClient
的源码比较长,但是它本身并没有特定功能,只是作为一个载体,记录下我们初始化的各种设置信息,比如缓存策略,读写重连超时时间,自定义拦截器等等。由于它实现了Call.Factory
接口,因此它还具备了将一个Request
转化为Call
的能力,依照注释Call
是一个已经准备好的,可在未来某个时间点执行的请求任务,它的具体实现类是RealCall
|
|
Request
类也是一个纯粹的载体封装了一些请求信息,这里就不贴源码分析了,然后开始执行请求
|
|
异步场景要相对复杂一些,这里先以分析异步为例,进入enqueue
方法
|
|
首先获取了client
中的调度分发器dispatcher
,它同时持有线程池和异步任务队列,会依照策略在适合的时机执行任务。AsyncCall
是对RealCall
的的内部类,持有RealCall
的引用,相当于对RealCall
做了一层包装,它继承自NamedRunnable
,本质是一个Runnable
,可被线程池执行。
|
|
enqueue
方法中先判断是否达到设置的并发上限(默认并发上限64,同一个主机地址并发上限5),未达上限则直接将任务交给线程池执行,并置入runningAsyncCalls
,达到上限则先加入readyAsyncCalls
等待处理。每当一个异步任务执行完成后都会调用promoteCalls
方法尝试从ready
队列中取出任务加入running
队列中执行。我们之前说过了AsyncCall
本质是一个Runnable
,那么线程池在执行该任务时会触发它的run
方法,进入AsyncCall
的源码
|
|
NamedRunnable
对Runnable
做了一层封装,run
方法会触发execute
方法的执行,看来整个请求最核心的部分就是getResponseWithInterceptorChain
方法了,它会直接返回结果response
,然后触发回调给到上层。最后在finally
代码块中会调用dispatcher
的finish
方法,前面的代码分析过每次异步任务执行结束后都会调用promoteCalls
做任务调度就是由这里触发的。
|
|
Interceptor
拦截器是OkHttp
中的核心组件,负责完成请求流程中的各项重要任务,并且采用了责任链模式
的设计方法,每一步都可能得到Response
,如果无法得到则将Request
传递给下一个Interceptor
,等待从下一个Interceptor
中传回Response
,如此将Interceptor
串联起来,这方面类似同样采取了责任链模式的Android事件传递机制。
Interceptor
的排列顺序也非常重要,比如最终负责从网络请求数据的Interceptor
必然放在最后,尝试从缓存中取数据或配置请求头部消息的Interceptor
必然要放在它之前,在上面的代码中就依序将Interceptor
放到了List中,并传递给了第一个Chain
,注意传递的参数中还有一个0,它代表index,从List中获取与当前Chain
对应的Interceptor
时就依靠这个值,下面进入RealInterceptorChain
的源码
|
|
省略了部分代码,既然叫做Chain
,那么它的作用只是帮助串联,逻辑是放到Interceptor
处理的,串联的关键就在proceed
方法中:
- 首先创建用于串联在当前
Chain
之后的下一个Chain
,并且让index+1,使得下一个Chain
中也能获得与之对应的Interceptor
- 然后根据当前index获取自己的
Interceptor
- 最后调用
intercept
方法,将下一个Chain
传入当前Interceptor
- 在
intercept
方法中,如果当前Interceptor
能够获得目标Response
则会直接将其返回,如果不能获取,则调用传入的下一个Chain
的proceed
,将获取Response
交给下一环,等待其结果并返回 - 然后就是循环以上过程
- request传递过程形如 当前Chain -> 当前Interceptor -> nextChain -> nextInterceptor
根据上面的分析,已经打通了整个请求过程。可见Request
会由起始到结尾(结尾不一定是CallServerInterceptor
,这里指实际执行到的最后一个Interceptor
)传递给每一个Interceptor
,最后Response
又会反向依次传递回来,在这个过程中Interceptor
可以分别对两者做各种处理。
下一节会分析各Interceptor
在各自的intercept
方法中是怎样做具体的逻辑处理的。
OkHttp拦截器分析
先列出之前在getResponseWithInterceptorChain
方法中添加的各Interceptor
,概括一下它们分别负责什么功能:
client.interceptors()
用户自定义的Interceptor
,能拦截到所有请求RetryAndFollowUpInterceptor
负责失败重连和重定向相关BridgeInterceptor
负责配置请求的头信息,比如Keep-Alive
、gzip
、Cookie
等可以优化请求CacheInterceptor
负责缓存管理,使用DiskLruCache
做本地缓存,CacheStrategy
决定缓存策略ConnectInterceptor
开始与目标服务器建立连接,获得RealConnection
client.networkInterceptors()
用户自定义的Interceptor
,仅在产生了网络请求时生效CallServerInterceptor
向服务器发出一次网络请求的地方
RetryAndFollowUpInterceptor
|
|
streamAllocation
对象是在这个Interceptor
中创建的- 在
followUpRequest
方法中判断了是否需要重定向以及是否需要重连,需要重连时会返回一个request
- request为null说明不需要重连,则直接返回
response
,否则由于while(true)
循环会再次执行chain.proceed
重新走一次请求流程
BridgeInterceptor
|
|
- 可以看到该
Interceptor
的内容几乎全是添加request头信息,没有什么需要分析的地方 - 包括启用长连接
Keep-Alive
,设置Cookie
,启用压缩与解压gzip
,相当于做了一些请求优化
CacheInterceptor
|
|
- 首先根据request从cache中取response
- 将request和response传入
CacheStrategy
根据缓存策略(比如仅使用网络加载,仅使用缓存,缓存时效等)得到经由策略处理后的networkRequest
和cacheResponse
- 若缓存策略要求仅从缓存中加载,且缓存未命中,则本次请求失败
- 若缓存策略不要求仅从网络获取数据,则直接返回缓存内容
- 以上条件均不满足,则把获得response的任务交给下一个
Chain
,开始执行网络请求 - 得到网络请求结果后,如果已经有缓存了,则用最新的网络数据更新缓存
- 最后将本次请求的结果response根据
cacheRequest
写入缓存
ConnectInterceptor
|
|
- 该
Interceptor
主要为下一步最终进行网络请求做铺垫,在这里获得了HttpCodec
和RealConnection
,凑齐了网络请求需要的参数,一起传入下一个Chain
- 在
newStream
方法中会去先尝试从RealConnectionPool
中寻找已存在的连接,若未命中则创建一个连接并与服务器握手对接 - 在完成连接后会将
Socket
对象通过Okio
封装成BufferedSource
和BufferedSink
,并将两者传入HttpCodec
,在下一步网络请求时会用到
|
|
CallServerInterceptor
|
|
- 在上面的源码中已经标注了4处最关键的点:发送request head、发送request body、获取response head、获取response body,其中body都是可选的,存在时才会发送/读取
- 可以发现具体实现都交给了
HttpCodec
,它是对Http协议操作的一种抽象,针对HTTP/1.1与HTTP2有Http1Codec
和Http2Codec
两种实现 - 方法的命名都是read和write,因为在
HttpCodec
中最后的请求和响应是由上一步封装的BufferedSource
和BufferedSink
来完成的,sink
负责输出流,将写入的数据交由socket发出,source
负责输入流,从socket中读取响应数据
OkHttp缓存相关
这节提一些与OkHttp缓存相关的内容,包括:
- 缓存策略
CacheStrategy
- 连接池
RealConnectPool
- 任务缓冲队列
Deque<AsyncCall>
缓存策略
之前在CacheInterceptor
中提到过CacheStrategy
类,但是没有细说,这里做一次详尽的分析,CacheStrategy
依赖于本地缓存Cache
和Http Header
缓存配置。
Cache
使用了DiskLruCache
作为缓存容器,以request.url
作为key来存储和读取response
,这是一种很常见的缓存方式,由于篇幅就不做分析了
Http Header
使用Http协议中约定的Cache-Control
、Expires
、ETag
、Last-Modified
、Date
等字段和服务端交互,由这些字段信息决定是否使用缓存,关于这些字段的含义可以查看Http协议中的定义,这里不做赘述
进入CacheStrategy
的源码
|
|
- 首先在
Factory
方法中获取它所依赖的参数,就是我们之前提到过的Cache中取到的缓存和Http Header配置信息 - 然后进入比较核心的方法
getCandidate
,在这里会根据之前拿到的依赖参数通过各种if判断返回不同的CacheStrategy
对象 - 本质上其实是返回不同的
networkRequest
和cacheResponse
,这样上层只需要关注这两个参数就知道下一步该如何做处理,复杂的判断都封装到了CacheStrategy
对外透明,具体判断过程在代码中做了注释
连接池
okhttp利用连接池来复用连接,避免反复握手建立连接,并且具备在合适的时候回收连接的能力,这也是okhttp设计出彩的地方之一,进入ConnectionPool
的源码
|
|
上面的代码虽然比较长,但是做了详尽的注释,概括的来说
- 连接池规定了
maxIdleConnections
最大闲置数量和keepAliveDurationNs
长连接维持时间(最长闲置时间)这两个参数,作为判断是否有连接可以回收的指标 - 连接池的本质是一个
Deque
容器,它开辟了一个线程池executor
专门用于执行连接回收任务cleanupRunnable
- 每当有一个新连接加入连接池时都会创建一个
cleanupRunnable
去执行回收任务,每当一个满足路由与地址的连接从池中取出用以复用时,都会使该connection的引用计数加一 - 关于connection的引用计数是一个
List<Reference<StreamAllocation>>
对象,每当用一个请求任务使用了该connection都会使List的成员加一,当任务结束时使List的成员减一,所以引用计数为0时说明当前connection处于闲置状态 cleanupRunnable
中有一个死循环,只有当前没有闲置连接需要被回收时才会跳出循环,否则会等待cleanup
方法返回的时间后再次执行cleanup
做回收cleanup
方法是实际执行回收逻辑的地方,它会先遍历连接池,找出闲置时间最久的连接,然后根据最早设定的maxIdleConnections
和keepAliveDurationNs
两个参数判断是否可以回收- 能回收则直接回收并返回0,这样死循环会再次执行
cleanup
做回收 - 不能回收会判断是否全部连接处于活跃,不是的话则计算当前待回收连接距离闲置上限时间的时间间隔,返回这个间隔,
cleanupRunnable
线程会wait这个时间后再执行cleanup
- 全活跃的话则返回闲置上限时间,线程等待这个时间后再执行
cleanup
- 最后判断如果当前已经没有连接了,则返回-1,会跳出死循环,结束当前这个
cleanupRunnable
- 能回收则直接回收并返回0,这样死循环会再次执行
以上就是连接池的运作原理
任务缓冲队列
这部分内容在上面第一节分析完整请求过程的Dispatcher.java类的解析中已经分析过了,包括使用Deque
作为任务队列容器,如何调度ready
和running
队列,线程池执行任务等等,这里不就再赘述了
最后
OkHttp篇完成٩(ˊᗜˋ*)و 依旧篇幅巨长,把想到的点都提了一下,尤其值得学习的是Interceptor
和ConnectionPool
这两个模块的设计,前者易扩展,后者高性能。敬请期待下一篇Retrofit篇~
参考文章
拆轮子系列:拆 OkHttp
浅析OkHttp3
OkHttp3源码分析综述
声明:本站所有文章均为原创或翻译,遵循署名-非商业性使用-禁止演绎 4.0 国际许可协议,如需转载请确保您对该协议有足够了解,并附上作者名(Est)及原贴地址