0%

08-Okhttp-【上】

一、Okhttp介绍

okhttp 4 的源码(4.9.3版本,它的包名还是 okhttp3,还是要注意下)

Retrofit 只是封装 Okhttp ,让其更容易使用

http 2.0 的特性:头部压缩、server push、多路复用

关于多路复用,其实Http 1.1 也有 keep-alive 这样的标记位来重复使用这条连接,但是这种保持连接只能第一个消息发送完收到 ack 之后,才能发送第二条数据,这种就是串行的;而 http 2.0 可以在第一条还未返回结果的时候就发送第二条,这种才是真正的多路复用。

因为 http1.1 用的是文本,必须要按照顺序, hello world,必须要按顺序,但是 http2.0 可以先传 r 再传h再传 d 等,它是基于 fragment 的,fragment 有顺序标识。

okhttp 支持 http2.0 可以复用连接,避免每次去连接都要三次握手之类的。Http2 还有个连接池,池子里面有各种连接,比如baidu.com 的连接、taobao.com 的连接,下次需要请求相关域名的时候,直接使用这个池子里面的连接即可。

okhttp 的 post 请求的缓存默认是关闭的,只有 get 请求的缓存开启。如果post 请求想开启缓存,只需要做如下 cache 配置就可以了:

1
OkhttpClient.Builder().cache(Cache(File("/xxx"), 1024))

二、Okhttp 的基本使用与请求流程

Okhttp 的大体流程如下:

  1. 创建一个 OkhttpClient(不管是自己Builder 还是直接new 都可以)

  2. 创建一个 Request ,封装请求数据

  3. Request 交给 OkhttpClient 的 newCall 操作得到一个RealCall

  4. 之后,不管是通过 execute 还是 enqueue 方法,反正最终是将这个call 交给 Dispatcher 去调度

  5. 被调度的时候,经过各种 interceptor 拦截器拦截后,最终可以得到 Response ,流程如下图所示:

Okhttp的执行流程

三、分发器异步分发限制

Dispatchers 里面维护了 3 个队列:

  • 准备执行的异步请求队列

  • 正在执行的异步请求队列

  • 正在执行的同步请求队列

AsyncCall 在 enqueue 的时候,会判断队列里面(不管是准备执行的还是正在执行的)有没有 call 和目前这个 AsyncCall 的host 是一样的,如果是一样的,那就将自己的 callsPerHost 的属性替换成队列中 Call 的 callsPerHost 属性(我个人理解的是,这样方便计数,所有的 Call 都用这一个AtomicInteger 变量来统计数量,这样,后续判断同一个 Host 最多只能5个来同时请求就比较好判断了,只需要取这个值)

所以, callsPerHost 表示的是同一个host 有多少个请求在执行

异步总连接不能超过64个,然后 同一个 host 的请求不能超过5个。

四、分发器异步请求分发流程

如果所有异步请求数超过 64 个 (或者同一个 host 的请求数超过 5个)的情况下,那么 call 将不会被执行,那么他们在什么时候会被触发去执行呢?其实,在 RealCall 里面的 run 方法里面,会在 try-catch 块去获取请求的 response :

1
2
//获取response 结果
response = getResponseWithInterceptorChain()

在最终的 finally 中,会执行 client.dispatcher.finished(this) ,告知 Dispatcher 当前 Call 已经结束。之后在 Dispatcher 中会再次激活这个等待队列:

1
2
3
4
internal fun finished(call: AsyncCall) {
call.callsPerHost.decrementAndGet()
finished(runningAsyncCalls, call)
}

可以看出,首先将 callsPerHost 减1 ,意味着这个 host 在执行的请求数减少一个。在里面还会执行重载的 finished 方法,再次去执行 promoteAndExecute 方法去从队列中获取 Call 去分发执行。所以,整个异步任务执行的工作流程如下所示:

异步请求执行的流程

线程池

Okhttp 异步连接的线程池初始化很有意思:

1
2
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false)

它指定 corePollSize 为 0,Keep Alive 的时长是 60s ,无界队列,这其实和我们使用 Executors.newCachedThreadPool 是一样的:

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

这种线程池有什么特点呢?

  • 核心线程数为 0 :线程池有个例外的情况,哪怕核心线程数设置为 0 ,在第一个任务的时候,还是会开启线程让执行。

  • 最大线程数设置为 Integer.MAX_VALUE ,几乎不限制任务数量

  • 使用 SynchronousQueue 队列提交任务。提交肯定失败!因为它不存储元素的。那既然提交任务肯定失败,那我们为什么还用这个呢?我们考虑下线程池,如果队列有容量的话,在线程数大于等于corePoolSize 的时候,新的任务会被添加到等待队列,只有在往 队列中添加失败的时候,才会去判断如果小于 maximumPoolSize 的时候,新建线程任务。所以,这种线程池可以让任务马上执行,而不是在那里等待。

所以上述线程池,如果设置corePoolSize 为 0 ,再设置个 LinkedBlockingQueue 的话,第一个任务会执行,第二个任务会放在 LinkedBlockingQueue 中。1 完成后,执行2,2执行后才能3等等。

五、分发器处理同步请求

只需要注意,同步请求执行完成后(finnaly 中也是调用 finish),也会去执行 promoteAndExecute 方法,去重新从等待队列中获取 Call 执行任务。有个细节注意,**同步任务执行完成后,会触发异步任务队列的重新获取!

六、Okhttp拦截器责任链设计模式

Okhttp 里面会有5个默认的拦截器,并且我们也能通过addInterceptor 和 addNetworkInterceptor 方法添加拦截器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
//添加我们通过 addInterceptor 方法添加的拦截器
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
//添加我们通过 addNetworkInterceptor 添加的拦截器
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
//省略无关代码。。。。。
}

从代码可以看出,通过 addInterceptor 添加的拦截器会首先被 add 进 interceptors 列表中,之后依次添加充实、桥、缓存、连接 拦截器,然后如果不是 websocket 的话,就添加通过 addNetworkInterceptor 添加的拦截器,最后添加的是 CallServerInterceptor 拦截器。

所以,addInterceptor 和 addNetworkInterceptor 方法添加拦截器的区别是什么?在代码中的体现是在 interceptors 数组中的位置不一样,一个是在最开始,一个是在倒是第二个。

但是这个顺序到底会有什么影响?Okhttp 使用责任链设计模式,在发起请求的时候,是从上往下经过这些 Interceptor ,这样就先经过我们 addInterceptor 添加的拦截器,后续才经过 addNetworkInterceptor 添加的拦截器。但是结果返回的时候,是从后面往前面返回的。

老师在课程里面讲了下如果自己去实现责任链的 Demo ,这个可以模拟下。

七、Okhttp 拦截器功能概述

首先总体概述一下各种拦截器的含义:

  • 重试定向拦截器。在交给下一个拦截器之前,判断用户是否取消了请求,如果已经取消了,就不需要将结果交回给用户了。并且,在返回结果之后,根据响应码判断是否需要重定向,如果满足条件就会重启执行所有的拦截器。

  • 桥接拦截器。在交出之前,负责将 HTTP 协议必备的请求头加入(比如host 等),并添加一些默认行为(比如 GZIP 压缩),在获得结果后,调用保存cookie 接口并解析GZIP 数据

  • 缓存拦截器。顾名思义就是交出之前读取并判断是否使用缓存;获得结果后判断是否需要缓存

  • 连接拦截器。从连接池中寻找一个连接,如果没有则创建一个连接,并获得对应的 Socket 流;在获得结果后不进行额外处理

  • 请求服务拦截器。进行真正的服务器通信,向服务器发送数据,解析读取响应数据。

刨根究底 addInterceptor 和 addNetworkInterceptor

当我们只是需要在请求参数里面添加字段,比如 sign 字段,或者在 response 里面修改一点数据,那么我们使用 addInterceptor 和 addNetworkInterceptor 都是可以的。

但是如果我们打印日志,使用了 httpLogingInterceptor 这个拦截器,打印请求数据和返回数据,如果你用 addInterceptor 添加拦截器,那么打印出来的是用户写的请求参数,如果使用的是 addNetworkInterceptor ,那么这个请求参数就还包括 Okhttp 自动补全的一些参数

八、Okhttp 相关的面试题

1、Okhttp 的分发器是如何工作的?

对于同步请求,只是记录一下;对于异步任务,首先添加到 ready 队列中,然后检查所有请求数小于64,以及同 host 请求数小于 5 是否满足,满足执行并添加到 running 队列中。

应用拦截器和网络拦截器有什么区别?

在 interceptors 中的顺序不同。导致接收到的数据完整性是不一样的。

谢谢你的鼓励