欢迎光临散文网 会员登陆 & 注册

DEVLOG 12.30 网络请求框架(上)-- OkHttp中的Dispatcher

2021-12-30 21:33 作者:房顶上的铝皮水塔  | 我要投稿

参考内容: Android程序员中高级进阶学习/OkHttp原理分析

写在前面:2021年就要结束了,这一年我从九月份开始写专栏,我把这里当成了自己的云上笔记,主要将B站的一些很好的学习视频进行总结,或者是写写自己的开发心得,竟然最后写了5w字,非常不可思议!感谢各位的阅读(但是好像人也不多哈哈。不过无所谓啦,这个是今年的最后一篇文章。当然,后续会继续写拦截器,以及OKHttp包装之后的Retrofit,Retrofit真的是写的非常出色! 还有对于阻塞队列、并发的学习心得,不过目前还没怎么学习 - _ -

这篇文章主要的内容:

  1. 分析OkHttpClient构建Call(请求)的过程

  2. 分析构建请求之后,OkHttp如果使用线程池API执行请求。


首先通过常见的使用进行源码分析:

我们通常构建一个OkHttpClient,通过OkHttp#newCall方法,结合我们构建的Request对象,执行execute(同步)或者enqueue(异步)进行请求,所以我们首先看看OkHttp#newCall方法。

OkHttp#newCall

Call接口

RealCall是OkHttp定义的一个类,它继承自Call。Call接口描述了一个准备好被执行的请求,我们可以通过Call的接口看看它能执行那些操作:

OkHttp实现了这里的Factory,所以我们可以通过Factory工厂方法创建Call对象。

RealCall#newRealCall

此处的EventListener描述的是一个Http请求的过程。通常来说,一个Http请求,包括DNS解析(通过域名获取IP地址),TCP/IP连接,然后再是应用层的Http请求,我们可以通过这个类的方法略知一二。

所以再看完Call对象的构建之后,我们来看另外两个非常重要的方法,分别是enqueue和execute。

RealCall#enqueue

enqueue方法解释了为什么OkHttp中的Call不能被执行两次。因为Call也存在生命周期,已经被执行的话,还执行一次会抛出异常。

此外,call被转交给Dispatcher#enqueue方法。Dispatcher#enqueue方法会根据条件选择是否立刻执行当前的call:

以上的条件主要是两个:

  1. 当前正在执行的Call的数量不能超过maxRequests(默认定义为64)

  2. 对于同一个host的请你去不能超过maxRequestsPerHost(默认定义为5)

跟踪代码发现这个host是使用HttpUrl类表示的,也就是我们常用的http url。

也就是说,如果满足条件,就会将请求存储到正在执行的一步请求队列(runningAsyncCalls),否则就存储到就绪队列(readyAsyncCalls)中:

readyAsyncCalls中的请求什么时候被执行?

有一个非常明显的事情,当我们使用OkHttp执行第一个请求的时候,这个请求肯定会被放入running队列中,并且我们的Call对象其实还被包装了一层AsyncCall:

我们的Call最后是要被送到线程池中执行的,这意味着execute方法会被回调,在execute方法中,拦截器帮助我们执行了最重要的Http请求,并且返回给我们Repsonse。在finally块里面,dispatcher调用了finish方法。当一个请求完成的时候,势必要去查看队列中是否还有没有被执行的任务:

Dispatcher可以通过promoteCalls,将ready队列中的请求拉出来执行:

这里会将ready队列中的请求,在满足不超过前面说的maxRequest和maxRequestPerHost的前提下取出来,放到running队列中,然后交给executorService执行。

彳亍,我们现在的目标就是要研究一下线程池是如何执行的我们的请求的。


使用线程池ExecutorService执行call

Executor构建了一个ThreadPoolExecutor作为线程池,一个线程池具有几个比较重要的参数:

  1. 核心线程数,这里设定为0

  2. 最大线程数,设定为max

  3. 线程存活时间。因为我们定义在线程池中的线程的数量可能大于最大线程数,如果大于最大线程数,并且还是空闲状态的这些线程,将会被移出。

  4. 任务队列,任务队列是一个BlockingQueue,它规定了任务的排队方式。这里的实现是SynchronousQueue。

RealCall#execute

execute方法和enqueue方法类似,根据上述的分析可以很快弄明白:


为什么要使用SynchronousQueue?

为什么在OkHttp中非要使用SynchronousQueue作为阻塞队列,使用fixed大小的队列,比如ArrayBlockingQueue,LinkedBlockingQueue为啥不行捏?这一节主要想说明这个问题。

之所以要使用SynchronousQueue作为阻塞队列,我们首先需要分析ThreadPoolExecutor执行任务的特点。(关于不同的阻塞队列的比较,我后面一定会写文章来说明)。

在ThreadPoolExecutor#execute中,ctl这个量其实是workerCount和runState这两个对于线程池【状态】的一个组合:

ctl是一个AtomicInteger,它的32位被拆分成这样两个部分,前面三位用于表示这几种状态:

workerCount表示线程池中的线程的数量,可以看到这个数量是非常庞大的,差不多有这么多:(2^29)-1 (about 500 million)。

ThreadPoolExecutor#execute

ok,稍微了解了ctl,我们来看看execute的代码:

对于一个随便的Runnable,我们首先会计算当前workerCount和核心数量比较,如果小于核心数量,就使用addWorker创建新线程;否则,加入workerQueue。这里的workerQueue是我们指定的阻塞队列。如果不能加入阻塞队列(通常是因为超过了固定的大小,比如ArrayBlockingQueue我们会指定大小),就会走到else if 分支,再调用addWorker创建新线程。 基于以上分析,我们上两个例子。

Case#1 使用ArrayBlockingQueue,大小设置为1,其他的设置和OkHttp相同:

输出结果:

并没有输出thread-2,如果按照上面的分析来看,因为core大小为0,第一个if直接pass,然后在第二个分支中,现将任务加入workQueue,此时发现workerCount为0,立刻执行任务1,workerQueue取出这个任务;当加入第二个任务时,和上面类似,workerQueue存入任务2,但是workercount不是0,所以第二个任务就被搁置了。

Case#2,在加入一个任务:

此时,在加入一个任务,在第三个任务进来之前,和上面分析相同,出现第三个任务,因为workerQueue大小为1,存了第二个任务,则第二个if没有命中,会走下面的addWorker,创建一条新的线程,先执行任务3,再执行任务2,我们来看看输出结果:

结果和我们分析的一样,2 3 任务是在另外的线程池上执行的。

通过这个现象,我们不难分析出使用ArrayBlockingQueue存在两个显著的局限性:

  1. 需要设置大小。如果前面的任务耗时,后面的任务会存在队列中,无法执行

  2. 就算可以执行了,执行顺序并非我们的预期。

基于以上两点分析,我们就选择了SynchronousQueue作为阻塞队列。(但是为什么这个队列可以保证顺序,之后的文章会分析)。


所以!我们来小结一下本篇文章:

  1.  我们通常使用的OkHttpClient是Call对象的一个工厂实现类,它通过newCall构建Call对象。

  2. 构建出的Call对象调用enqueue方法或者是execute方法时,都会转到Dispatcher的enqueue方法或者是execute方法中。

  3. Dispatcher内部使用了ThreadPoolExecutor作为线程池。该线程池的阻塞队列实现使用了SynchronousQueue,使用这个队列是为了保证请求的顺序和请求都被执行。


DEVLOG 12.30 网络请求框架(上)-- OkHttp中的Dispatcher的评论 (共 条)

分享到微博请遵守国家法律