OkHttp 分发器
本文概述
- 文章以OkHttp 分发器、拦截器为讨论重点;深入源码层面系统分析了分发器的工作流程(应对同/异步请求)并讨论了OkHttp 中拦截器的责任链设计模式以及OkHttp 默认五大拦截器;文末给出了部分面试题解题思路;
正文部分:
-
整体流程
-
实例化OkHttpClient 对象
-
实例化Request 对象
-
实例化newCall(request):得到的是ReCall 对象
- 源码:
-
Call:是一个接口,包含方法
- excute:同步
- enqueue:异步
-
-
异步请求中干了什么
- 源码展示
-
判断这个Recall 是否被执行:check
- 已经被执行过了 ---> 抛出异常
- 底层使用AtomicBoolean (默认值为false)
- 第一次执行(无论同步/异步) ---> 将这个值改为true
- 第二次执行(无论同步/异步) ---> 抛出异常
-
事件回调:callStart()
- 在构建OkHttpClient 的时候如果配置了eventListener(),在请求的时候就会回调
- 配置eventListener
-
有什么事件
-
缓存:
- 缓存命中
- 缓存miss
-
请求:
- 请求开始
- 请求结束
- 请求失败
-
任务:取消任务
-
-
分发器分发请求
-
其实在构建OkHttpClient 的时候就可以去定制分发器
- 可以去修改参数,但一般情况下不去改
-
传入参数AsyncCall
- 这个是内部类并且实现了Runnable 接口
- 存在类属性:callsPerHost : AtomicInteger
-
-
分发器的enqueue 方法干了什么事情 :从ready 队列移到running 队列
-
分发器:异步请求工作流程
-
拿到AsyncCall 代表异步请求任务(是一个Runnable ),交给Dispatcher 分发
-
先将AsyncCall 放入ready 队列等待执行
-
执行promoteAndExcute
-
对ready 队列进行遍历,判断是否满足限制
-
当前的满足,从ready 队列中拿出AsyncCall 给running 队列
-
当前的不满足,检查ready 队列中的下一个AsyncCall
- 都不满足,什么都不干
-
-
-
某一个任务(AsyncCall )执行完成
-
从running 队列中将其移除
-
执行分发器的finish 方法
- 执行promoteAndExcute (重复刚才的流程)
-
-
-
将call 放入准备执行的异步请求队列
-
此时存在三个队列(关注两个异步请求队列)
-
为什么会存在两个异步请求队列:对庞大的请求进行限制
-
-
如果此时为Http 请求
-
查找两个异步队列中有没有跟当前请求的目标主机相同的请求
-
有,就拿到这个AsyncCall 对象
-
将当前AsyncCall 对象的callsPerHost 赋值为找到的这个AsyncCall 的callsPerHost
- 使得两个AsyncCall 对象中的callsPerHost 相同
-
-
-
分发请求:promoteAndExecute
-
迭代准备执行的异步请求队列
while (i.hasNext()) { val asyncCall = i.next()
-
限制正在执行异步请求的任务数 不能大于 64个,跳出迭代
- 不可能一下子异步干10000 个请求
//正在执行异步请求的任务数 不能大于 64个 if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
-
限制同一个host的请求数 不能大于5
//同一个host的请求数 不能大于5 // Host max capacity. if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue
- 可以存在6 个对百度的请求,但第6 个请求需要等待前五个正在执行的请求让出一个位置才能执行
- 缓解服务端的压力
-
-
-
将拿到的任务从ready 队列中移除:准备开始执行这个任务了
i.remove()
-
callsPerHost 加一
- 这个记录的是:存在多少个相同host 的正在执行的请求数
asyncCall.callsPerHost.incrementAndGet()
-
将这个任务添加到临时集合:设计亮点
executableCalls.add(asyncCall) //需要开始执行的任务集合
-
会扫描这个队列,准备执行:executeOn
for (i in 0 until executableCalls.size) { val asyncCall = executableCalls[i] asyncCall.executeOn(executorService) }
-
executeOn 传入线程池,执行这个AsyncCall 的run 方法
-
-
将这个任务添加到正在执行的异步请求队列中
runningAsyncCalls.add(asyncCall)
-
存在的问题:
-
如果当前任务不满足上述的两个限制那么就会进入等待状态,那这个等待状态的Async 什么时候执行呢?
- 在Async 的run 方法中重新进行启动
//执行请求 ---> 实质上是执行拦截器 val response = getResponseWithInterceptorChain()
-
接着会调用
client.dispatcher.finished(this)
-
首先对callsPerHost 减一
call.callsPerHost.decrementAndGet()
-
执行重载的finish 方法
- 当有65 个请求,结束了一个请求后 ---> 调用finish ,重新启动任务,将原第65 个请求放进来(从ready 队列取)
finished(runningAsyncCalls, call)
-
再次执行promoteAndExecute 启动任务
val isRunning = promoteAndExecute()
-
-
-
线程池的执行小细节
-
当一个任务通过execute(Runnable)方法添加到线程池时:
- 线程数量小于corePoolSize,新建线程(核心)来处理被添加的任务;
-
线程数量大于等于 corePoolSize,新任务被添加到等待队列,若添加失败:
- 线程数量小于maximumPoolSize,新建线程执行新任务;
- 线程数量等于maximumPoolSize,使用RejectedExecutionHandler拒绝策略。
-
-
Dispatcher 中的线程池长什么样子
-
核心线程数:0
-
最大线程数:Int.MAX_VALUE
-
保活(闲置)时间:60 秒
-
阻塞队列:无界队列 设计亮点
-
向里面提交任务一定会失败 :希望提交的任务能得到及时执行而不等待
- 如果提交成功 ---> 一定会等待执行
- 如果提交失败 ---> 新建线程,马上执行这个任务
-
-
这个跟newCachedThreadPool 是相同的
- 让请求并发执行,不能让它等
-
-
OkHttp 同步请求:execute
-
请求流程:
-
check:判断此请求是否初次执行
check(executed.compareAndSet(false, true)) { "Already Executed" }
-
callStart():事件回调
-
分发器分发:
client.dispatcher.executed(this)
-
-
同步请求的分发器是怎么玩的
-
将当前请求任务放入同步请求队列
@Synchronized internal fun executed(call: RealCall) { runningSyncCalls.add(call) }
- 同步请求没有等待队列,来就直接干
-
执行这个请求:拿到请求的结果Response
- 异步请求还是调用这个方法获得对应的响应结果
return getResponseWithInterceptorChain()
-
执行同步队列的finish 方法:同样会触发ready 队列到running 队列的检查
- 在OkHttp 3.X 中就不会在同步请求结束后触发异步任务的检查
-
从同步队列中移除这个请求
-
OkHttp 拦截器责任链设计模式
-
首先:
-
OkHttp 通过分发器调用getResponseWithInterceptorChain 拿到同/异步请求的Response
-
添加拦截器:
-
创建一个拦截器集合
val interceptors = mutableListOf<Interceptor>()
-
添加自定义拦截器:addInterceptor
//添加到前面 interceptors += client.interceptors
-
添加默认的四个拦截器
interceptors += RetryAndFollowUpInterceptor(client) interceptors += BridgeInterceptor(client.cookieJar) interceptors += CacheInterceptor(client.cache) interceptors += ConnectInterceptor
-
如果不是WebSocket 类型的请求的话,添加自定义拦截器
//适用于Http 请求 if (!forWebSocket) { interceptors += client.networkInterceptors }
-
添加最后一个默认的拦截器
interceptors += CallServerInterceptor(forWebSocket)
-
注册自定义拦截器,是可以拿到chain 链条(一定调用proceed 方法)
- 不然,不成连;
-
问题:为什么两种自定义拦截器添加的时机不同
-
拿到请求,拿到响应的时机不同;
-
有个HttpLoggingIntercept (日志)
- 记录用户的请求:放到前面
- 记录真正的请求:放到后面
-
-
拦截器的应用场景:签名
- 根据请求对象/URL ---> 带了签名的请求对象/URL;
- 因为请求是一定会经过自定义拦截器的(类似RxJava 的卡片式编程)
-
-
示意图:
-
-
OkHttp 的默认拦截器种类
-
重试重定向:RetryAndFollowUpInterceptor
-
处理桥接:BridgeInterceptor
- 处理请求/响应头信息
-
处理缓存:CacheInterceptor
- 缓存中存在那种不会改变的资源正好是想要的,那就不用发起这次请求
-
网络请求拦截器:ConnectionInterceptor
- 适用于缓存没有直接命中
- 根据跟服务器的链接对象去执行最后一个拦截器
-
服务器通讯拦截器:CallServerInterceptor
- 根据网络请求拦截器拿到的与服务器的链接对象,将请求的数据(requst),将request 编码成Http 报文,交给Sockect 的OutputStream 发给服务端获得响应
-
-
OkHttp 拦截器执行流程
-
责任链模式(对象行为型模式),请求自顶向下,响应从下至上
- 对象行为型模式,为请求创建了一个接收者对象的链,在处理请求的时候执行过滤(各司其职)。
- 责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。
-
生活场景:吃外卖的我,不关心这个外卖盐是放了多少,等着吃就行了
-
OkHttp 默认五大拦截器
-
工作流程:
-
重试拦截器:
-
在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;
- 拦截器结束这个请求,不向下走
-
在获得了结果之后 ,会根据响应码判断是否需要重定向
- 拦截器结束这个请求,不回馈给用户;
-
如果满足条件那么就会重启执行所有拦截器。
-
根据Http 状态码:3XX
- 将重定向的URL 组成新的URL 执行重定向
-
-
-
桥接拦截器:
-
在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)
- Http 请求必须携带Host 请求头
- OkHttp 在实例化OkHttpClient 时不用显示添加Host 请求头,桥接拦截器会做
-
并添加一些默认的行为(如:GZIP压缩);
- 服务端在进行GZIP 压缩了数据后,会通过响应头告诉客户端
- 客户端会进行GZIP 解析
-
在获得了结果后,调用保存cookie接口并解析GZIP数据。
-
在实例化OkHttpClient ,可以添加CookieJar
-
saveFromResponse
- 响应存在Cookie 数据,桥接拦截器回调这个方法,将对应的URL 以及Cookie 数据回传给用户 ,由用户完成保存
-
loadFromResponse
- 在请求时,桥接拦截器会回调这个方法,根据请求的url 判断有没有Cookie 需要设置给这个URL 请求头中去
-
第二次请求相同的网站,将Cookie 拿出去
-
-
-
-
缓存拦截器:
- 顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
-
连接拦截器:
- 在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。
- 连接池中有就拿,没有就新建一个
-
请求服务器拦截器:
- 进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。
面试题汇总:
-
OkHttp 的请求流程是什么样的
-
实例化OkHttpClient 对象,实例化Request 对象;将Request 对象作为参数实例化一个实现了Call 接口的RealCall 对象;RealCall 存在同步/异步方法,但只能执行一次
-
执行同步请求:在拦截器中会将该RealCall请求保存在正在执行的同步请求队列中去
-
RealCall.execute
- 分发器中:只是将正在执行的请求放入同步队列
- 拦截器中:执行完后就移除这个任务
-
-
执行异步请求:根据RealCall 生成AsyncCall ,将其交给分发器;
-
在分发器中:
-
拿到AsyncCall 代表异步请求任务(是一个Runnable ),交给Dispatcher 分发
-
先将AsyncCall 放入ready 队列等待执行
-
执行promoteAndExcute
-
对ready 队列进行遍历,判断是否满足限制
- 限制一:正在执行异步请求的任务数 不能大于 64个,跳出迭代
- 限制二:同一个host的请求数 不能大于5
-
当前的满足,从ready 队列中拿出AsyncCall 给running 队列
- 拿给线程池执行,进入拦截器处理
-
当前的不满足,检查ready 队列中的下一个AsyncCall
- 都不满足,什么都不干
-
-
某一个任务(AsyncCall )执行完成
-
从running 队列中将其移除
-
执行分发器的finish 方法
- 执行promoteAndExcute (重复刚才的流程)
-
-
-
在拦截器中:请求自顶向下,响应自下而上;
-
-
-
拦截器是如何进行工作的:
-
OkHttp 拦截器执行流程
-
责任链模式(对象行为型模式),请求自顶向下,响应从下至上
- 对象行为型模式,为请求创建了一个接收者对象的链,在处理请求的时候执行过滤(各司其职)。
- 责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以责任链将请求的发送者和请求的处理者解耦了。
-
-
-
应用拦截器与网络拦截器的区别
-
应用拦截器:自定义拦截器一
-
网络拦截器:自定义拦截器二
-
区别:
- 应用拦截器:第一个拿到请求(用户的请求),最后一个拿到响应
- 网络拦截器:倒数第二个拿到请求(真正发出去的Request对象),第二个拿到响应
-