0
点赞
收藏
分享

微信扫一扫

OKHttp源码解析(一)分发器


一.OkHttp介绍

OkHttp是由Square公司贡献的一个处理网络请求的开源项目,是目前Android使用最广泛的网络框架。使用它有以下好处

1.支持HTTP2.0并允许对同一主机的所有请求共享一个套接字
2.通过socket连接池,减少了请求延迟
3.默认通过GZip压缩数据
4.响应缓存,避免了重复请求的网络
5.请求失败自动重试主机的其他ip,自动重定向

至于如何实现这些的,相信看完整个OkHttp的介绍,你就清楚了

二.OkHttp的基本使用(以Get请求为例)

引入依赖。

1.首先创建OkHttp(具体实现为OkHttpClient

OKHttp源码解析(一)分发器_阻塞队列

2.然后创建请求

OKHttp源码解析(一)分发器_android_02


3.用OkHttpClient,将请求封装成​call​

OKHttp源码解析(一)分发器_阻塞队列_03


4.执行请求(有两种,execute为同步执行,​enqueue​为异步执行。这里以同步执行为例)

OKHttp源码解析(一)分发器_线程池_04


5.接下来就可以拿response做事了

三.OkHttp的角色分析,以及调用流程

对上述OkHttp的基本使用的介绍可以得出,OkHttp至少涉及四个基本角色,即​​OkHttpClient,Request,Call,Response​​。

那么这四个角色的调用顺序是什么呢?

用这张图可以概括

OKHttp源码解析(一)分发器_异步请求_05


Call之前的就不用过多介绍了,Call之后

请求可以通过​​execute​​​发出,也可以通过​​enqueue​​​发出。前者是同步请求,会阻塞线程。后者是异步请求。不管怎么样,都会发出请求。那么请求真正发出之前,会调用一个分发器来调配请求,也就是图中的​​Dispatcher​​​,将请求分发好了之后,就真正地发出请求,其间会通过拦截器,也就是图中的​​Interceptors​​。最后得到响应。响应完了之后,分发器又会去取请求,再真正的发出,以此类推

所有的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务

现在还有点懵?没关系,下面看源码,一步一步地分析,就很清晰了

四.以异步请求为例,研究OkHttp的分发器

在上面的基本使用中可以看到,​​Request​​​是通过​​OkHttpClient​​​的​​newCall​​方法传入的,我们追踪源码

OKHttp源码解析(一)分发器_线程池_06


发现它返回的​​RealCall​​​对象(​​RealCall​​​就是​​Call​​​的一个实现类,​​Call​​​是一个接口)所以说后面的​​call.execute​​​或者是​​call.enqueue​​​都是调用的​​RealCall​​​的同名方法,因为我们这里以异步请求为例,所以找到​​RealCall​​​的​​enqueue​​方法

OKHttp源码解析(一)分发器_android_07


我们进入分发器,看看它的​​enqueue​​​方法怎么处理的​​AsyncCall​

OKHttp源码解析(一)分发器_异步请求_08


这里的异步请求的处理,可能会有两种情况,一是进入​​runningAsyncCalls​​队列,然后直接调用线程池去执行该请求。二是进入​​readyAsyncCalls​​队列,等待被执行。用图来表示的话就是这样

OKHttp源码解析(一)分发器_线程池_09


那么怎么判断进入哪个队列呢?看​​if​

OKHttp源码解析(一)分发器_异步请求_10


前者就是,​​runningAsyncCalls​​队列的大小是否已经超过临界值(即64)。后者就是,对同一个host(主机)的请求是否超过了临界值(即5)。

前者好理解,后者的意思是,当前在​​runningAsyncCalls​​队列中已经有5个对www.baidu.com的请求了,再来一个就是6个了,就不行了。前者是对客户端的保护,后者是对服务器的保护,你细品。那么​​readyAsyncCalls​​​队列中的任务,何时被执行呢?我们看在​​runningAsyncCalls.add(call)​​​下面,调用了线程池去执行​​call​​​。这个​​call​​​就是一个​​Runnable​​​,执行​​call​​​也就是调用​​call​​​的​​run​​​方法,​​call​​​是​​AsyncCall​​​类,所以是调用​​AsyncCall​​​的​​run​​​方法。但是去​​AsyncCall​​​类中找,没有​​run​​​方法。所以去​​AsyncCall​​的父类当中找

OKHttp源码解析(一)分发器_android_11


进入​​NamedRunnable​

OKHttp源码解析(一)分发器_android_12


可以看到有​​run​​方法,​​run​​方法调用了​​execute​​方法,也就是调用了​​AsyncCall​​的​​execute​​方法

OKHttp源码解析(一)分发器_异步请求_13


在这里,​​finally​​中的代码是一定会执行的。所以就会调用分发器的​​finished​​方法,找到该方法

OKHttp源码解析(一)分发器_异步请求_14


我可以直接说,该方法就是把​​readyAsyncCalls​​中的​​Call​​移动到​​runningAsyncCalls​​中的。那么具体怎么移的呢?

首先调用重载的​​finished​​方法。三个参数分别是​​runningAsyncCalls​​,刚执行完的请求,​​true​​。因为该请求已经执行完了,所以利用​​remove​​方法将该请求从​​runningAsyncCalls​​队列中移除,之后调用​​promoteCalls​​方法,从​​readyAsyncCalls​​中取​​calls​​。进入​​promoteCalls​​方法

OKHttp源码解析(一)分发器_线程池_15


这个方法就是具体将​​readyAsyncCalls​​中的​​call​​移动到​​runningAsyncCalls​​中的。代码的意思我已经在注释中写的很清楚了,就不再赘述了

所以,对于异步请求,它的执行过程是这样的

OKHttp源码解析(一)分发器_android_16

runningAsyncCalls和readyAsyncCalls的数据结构

是双向队列,即​​ArrayDeque​​。即两边都可以插入和删除。但是在目前的版本中,貌似没有用到双向队列的功能。我猜是方便后面的扩展。

五.同步请求的过程

进入​​RealCall​​​的​​execute​​方法中

OKHttp源码解析(一)分发器_阻塞队列_17


进入​​executed​​方法

OKHttp源码解析(一)分发器_android_18


可以看到它直接将​​call​​​加入到​​runningSyncCalls​​​(注意,不是​​runningAsyncCalls​​队列)中,按先后顺序一个一个地执行了,这里就不用使用线程池了,因为是一个一个地执行

六.OkHttp对线程池的应用

在上面对异步请求的源码分析中,有这么一段

OKHttp源码解析(一)分发器_线程池_19

那么OkHttp对线程池又做了怎样的使用呢?目的是什么?

在分发器中,找到了创建线程池的方法,可以发现两个疑点

第一个是核心线程数为0,最大线程数为Integer.MAX_VALUE

第二个是阻塞队列采用的是​​SynchronousQueue​

OKHttp源码解析(一)分发器_异步请求_20


为什么这样用呢?

首先看下线程池的三种阻塞队列

①​​ArrayBlockingQueue​​ 看下面一段代码。

首先创建阻塞队列,容量为1,然后创建核心线程数为0,最大线程数为Integer.MAX_VALUE,且阻塞队列为​​ArrayBlockingQueue​​的线程池.。然后分别执行两个任务,第一个任务是死循环,第二个任务是一个输出任务。

OKHttp源码解析(一)分发器_线程池_21


这段程序运行的结果为:

OKHttp源码解析(一)分发器_异步请求_22


只输出了任务1

原因:看线程池的源码

OKHttp源码解析(一)分发器_线程池_23


按线程池的执行逻辑,因为核心线程为0,所以会先把任务1加到阻塞队列,然后再判断,当前work的线程为0,所以要创建一个线程,去执行任务1,因为任务1是死循环,所以就跳不出来了。所以任务2会一直待在阻塞队列中,得不到执行。当再来一个任务的时候,即任务三,会出现什么情况呢?

OKHttp源码解析(一)分发器_android_24


会发现任务1仍然会阻塞,但会先执行任务3,再执行任务2,且任务3和任务2在同一个线程中执行。

原因:因为线程3来的时候,阻塞队列已满,所以又会创建一个新线程,先执行任务三,然后再把阻塞队列当中的任务执行完。

这个阻塞队列,必须得初始化容量

②​​LinkedBlockingQueue​

这个阻塞队列和第一个阻塞队列的底层区别就是,这个阻塞队列是链表。

还有一个区别是,​​LinkedBlockingQueue​​可以不初始化容量,当没有初始化的时候,容量就是Integer.MAX_VALUE,所以如果还是上面的代码,加上任务3之后,任务3和任务2都不会执行,都会加入到阻塞队列当中去。

③​​SynchronousQueue​

这个阻塞队列,无容量。这样再配合核心线程数为0,就能达到最大并发量的效果。来一个任务,就能立马执行,而无需等待。当然也不会真的达到Integer.MAX_VALUE,因为有64的限制

使用线程池,就是为了线程复用。当然如果使用前两个阻塞队列,把容量改为0,好像貌似也能达到相同的效果?

解决问题

之所以这么用线程池,就是为了达到无等待,最大并发的效果


举报

相关推荐

0 条评论