1、
okhttp的任务调度主要是通过dispatcher来实现的。
okhttp在外部同步调用的是execute()方法,异步调用的是enqueue()方法,那么okhttp是如何实现同步和异步的区别呢?
主要是通过dispatcher这个类来完成的。发送的同步、异步请求都会在dispatcher中管理其状态。
2、
什么是dispatcher?
dispatcher的作用就是用于维护同步和异步的请求状态的,同时在内部还维护了一个线程池,用于执行相应的请求。而okhttp的同步和异步请求,都用到了dispatcher。异步使用dispatcher比同步更复杂一些。dispatcher主要的作用就是维护任务队列。
3、
dispatcher这个类用于维护请求状态,这个请求包括了同步和异步的请求,同时内部维护了一个线程池。每当有网络请求的时候,通过call进行封装,通过dispatcher把请求推送到请求队列readyAsyncCalls中。okhttp比其它网络框架高效在它内部维护了一个线程池,它能够更高效地执行异步请求。而这个线程池就是维护在dispatcher这个类里面的。当请求过来的时候,dispatcher会根据它的请求逻辑进行选择。
4、dispatcher的源码
首先,它维护了一个runningAsyncCalls这个队列,表示正在执行的异步请求,它包含了已经取消但是没有执行完成的异步请求。
第二个队列readyAsyncCalls这个就绪状态的异步请求队列,当一个请求不满足请求状态的时候就会进入到就绪请求状态进行缓存,当条件再满足了,就会把请求从就绪状态队列readyAsyncCalls放到正在执行的队列runningAsyncCalls这个队列中进行执行。
还有一个很重要的就是线程池ExecutorService,dispatcher正是通过线程池维护了整个异步请求还有进行高效的网络操作,这就是它非常重要的三个变量。
结合生产者消费者理论,每当有新的服务请求Call到Dispatcher这个管理类的时候,通过调用call.enqueue()方法的时候,会通过判断readyAsyncCalls最大请求数在不在范围内,以及相同主机最大请求数是不是在范围内这两个条件进行判断,如果是的话就进入到正在执行的任务队列并立即执行;如果不满足的话,也就意味着消。费者存满了,就进入到就绪队列中等待运行队列中有空间了,再把缓存队列的请求放到正在执行的队列中执行。同时,再任务执行完以后,会调用finished里面一个promoteCall()这个调度任务的方法,它会手动地清除缓存区。
5、
异步请求为什么需要两个队列?
一个是等待执行的缓存的异步请求队列,还有一个是正在直接执行的异步请求的队列。可以把它理解为生产者、消费者模型。Dispatcher对应生产者,dispatcher不是在子线程的,默认在主线程的。
ExecutorService:线程池,消费者池
把okhttp的dispatcher模型理解为生产者、消费者模型。既然是生产者、消费者模型的话,就需要两个队列来存放这些异步请求和等待执行的异步请求,也就是下面两个队列,一个是用于缓存的,一个是用户保存正在执行的异步请求的
Deque<readyAsyncCalls>:缓存
Deque<runningAsyncCalls>:正在运行的任务
6、从同步发送请求代码中开始看
同步请求中调用的是execute()这个方法,
在realCall中的具体实现。在realCall中还是通过client.dispatcher().executed()这个类来实现的。
在同步请求中,dispatcher做的非常简单,就是一个简单的集合添加操作,它表示每当有新的请求添加进来的时候,会直接加到同步的运行队列当中,而不是像异步一样考虑各种情况。
7、在异步请求中dispatcher的操作
在调用call.enqueue()方法的时候,其实调用的是realcall的enqueue()方法
而realCall其实也是一个空壳,具体的操作也是通过dispatcher来操作的。
dispatcher的enqueue()方法。
如果异步请求的个数小于最大请求个数并且当前host下的请求数小于设定的最大host请求数,者时候可以把封装好的AsyncCall添加到正在执行的异步请求队列当中,然后交给线程池去执行。线程池会负责线程的创建、销毁等管理。
如果不满足判断条件的话,会将异步请求添加到就绪的异步请求缓存队列当中进行缓存等待的操作。
这个代码不复杂,但是通过dispatcher来封装好了所有的请求管理,帮你做了所有的操作,这就是okhttp的核心dispatcher。
那么线程池到底是如何执行的?
其实它的创建和一般的线程池的创建都是类似的,但是前三个参数有区别。分别代表了核心线程池的数量(设置为0,意义是空闲一段时间后就会将线程全部销毁)、最大线程数(设置为整数的最大值Integer.Max,意义是当请求过来后,可以无限地扩充最大线程数,当然,这里只是理论上可以无限扩充,但是由于实际上受到okhttp设置的最大请求数的限制,所以也不是可以无限创建线程的)、第三个参数代表当线程数大于核心线程数的时候,多余的空闲线程可以存活的最大时间,也就是60s(设置为60)。那么这个线程池设置这三个参数到底意味着什么?比如,在实际运行中可能开启了十几、二十几个请求,线程池因此也会创建20几个线程,那么当工作完成之后,线程池会在60s之后相继关闭所有无效的线程。这就是这个线程池设置这三个参数的意义。
8、
dispatcher的作用就是维护请求队列,不管是异步的也好,同步的也好。dispatcher中维护了一个线程池,用于执行异步的网络请求,当满足条件的时候,就是满足最大请求数不大于64并且对应对应请求的host数目不超过5时,就会直接把异步请求放入正在执行的异步请求队列中来执行,通过线程池来开启任务。这里的call本质上就是一个runnable。如果不满足请求条件,就会把异步请求放入就绪的异步请求队列readyAsyncCalls中,当异步请求队列中有空闲的位置时,就会从就绪缓存列中取出优先级高的请求放入执行队列来执行。
9、
入队时把异步请求封装成了AsyncCall对象,而AsyncCall其实是一个runnable,既然它是一个runnable,执行的时候肯定是放在一个线程池中操作的。那么AsyncCall的线程执行方法是什么?
在run方法中调用了AsyncCall的execute()方法
在这个方法中,在finally代码块中,最终都会调用方法client.dispatcher().finished(this),无论有没有异常。方法的前半部分主要是对返回结果进行相应的回调,
finished()方法最终调用了三个参数的finished方法。
第一步就是调用的calls.remove(call)这个方法,这个方法就是将我们的异步请求从我们的执行队列中删除。
第二步调用的是promote Calls()这个方法,这个方法的作用是用来调整我们的任务队列的。不管是异步请求的执行队列还是异步请求的缓存队列,都是线程不安全的,所以调用这个方法时都会在同步代码块中执行。
第三步调用的是runningCallsCount()这个方法,这个方法其实就是重新计算了正在执行的线程数量,
promoteCalls()主要作用
在这里对异步请求的缓存队列进行调度。
在for循环中,首先会对原来的异步请求缓存队列进行遍历,最终会调用它的remove方法,把最后一个元素取出并移除之后,添加到正在运行的异步请求队列中,然后通过线程池执行。从异步请求缓存队列中移除并添加到异步请求执行队列的前提是,请求数没有超过异步请求数没有超过最大值、请求主机数也没有超过最大请求主机数,并且等待执行的异步缓存队列中有任务。
注:
runningCallsCount其实就是返回正在执行的异步请求和同步请求数量