一.OkHttp介绍
OkHttp是由Square公司贡献的一个处理网络请求的开源项目,是目前Android使用最广泛的网络框架。使用它有以下好处
1.支持HTTP2.0并允许对同一主机的所有请求共享一个套接字
2.通过socket连接池,减少了请求延迟
3.默认通过GZip压缩数据
4.响应缓存,避免了重复请求的网络
5.请求失败自动重试主机的其他ip,自动重定向
至于如何实现这些的,相信看完整个OkHttp的介绍,你就清楚了
二.OkHttp的基本使用(以Get请求为例)
引入依赖。
1.首先创建OkHttp(具体实现为OkHttpClient
)
2.然后创建请求
3.用OkHttpClient
,将请求封装成call
4.执行请求(有两种,execute
为同步执行,enqueue
为异步执行。这里以同步执行为例)
5.接下来就可以拿response
做事了
三.OkHttp的角色分析,以及调用流程
对上述OkHttp的基本使用的介绍可以得出,OkHttp至少涉及四个基本角色,即OkHttpClient,Request,Call,Response
。
那么这四个角色的调用顺序是什么呢?
用这张图可以概括
Call之前的就不用过多介绍了,Call之后
请求可以通过execute
发出,也可以通过enqueue
发出。前者是同步请求,会阻塞线程。后者是异步请求。不管怎么样,都会发出请求。那么请求真正发出之前,会调用一个分发器来调配请求,也就是图中的Dispatcher
,将请求分发好了之后,就真正地发出请求,其间会通过拦截器,也就是图中的Interceptors
。最后得到响应。响应完了之后,分发器又会去取请求,再真正的发出,以此类推
所有的逻辑大部分集中在拦截器中,但是在进入拦截器之前还需要依靠分发器来调配请求任务。
现在还有点懵?没关系,下面看源码,一步一步地分析,就很清晰了
四.以异步请求为例,研究OkHttp的分发器
在上面的基本使用中可以看到,Request
是通过OkHttpClient
的newCall
方法传入的,我们追踪源码
发现它返回的RealCall
对象(RealCall
就是Call
的一个实现类,Call
是一个接口)所以说后面的call.execute
或者是call.enqueue
都是调用的RealCall
的同名方法,因为我们这里以异步请求为例,所以找到RealCall
的enqueue
方法
我们进入分发器,看看它的enqueue
方法怎么处理的AsyncCall
这里的异步请求的处理,可能会有两种情况,一是进入runningAsyncCalls
队列,然后直接调用线程池去执行该请求。二是进入readyAsyncCalls
队列,等待被执行。用图来表示的话就是这样
那么怎么判断进入哪个队列呢?看if
前者就是,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
的父类当中找
进入NamedRunnable
可以看到有run
方法,run
方法调用了execute
方法,也就是调用了AsyncCall
的execute
方法
在这里,finally
中的代码是一定会执行的。所以就会调用分发器的finished
方法,找到该方法
我可以直接说,该方法就是把readyAsyncCalls
中的Call
移动到runningAsyncCalls
中的。那么具体怎么移的呢?
首先调用重载的finished
方法。三个参数分别是runningAsyncCalls
,刚执行完的请求,true
。因为该请求已经执行完了,所以利用remove
方法将该请求从runningAsyncCalls
队列中移除,之后调用promoteCalls
方法,从readyAsyncCalls
中取calls
。进入promoteCalls
方法
这个方法就是具体将readyAsyncCalls
中的call
移动到runningAsyncCalls
中的。代码的意思我已经在注释中写的很清楚了,就不再赘述了
所以,对于异步请求,它的执行过程是这样的
runningAsyncCalls和readyAsyncCalls的数据结构
是双向队列,即ArrayDeque
。即两边都可以插入和删除。但是在目前的版本中,貌似没有用到双向队列的功能。我猜是方便后面的扩展。
五.同步请求的过程
进入RealCall
的execute
方法中
进入executed
方法
可以看到它直接将call
加入到runningSyncCalls
(注意,不是runningAsyncCalls
队列)中,按先后顺序一个一个地执行了,这里就不用使用线程池了,因为是一个一个地执行
六.OkHttp对线程池的应用
在上面对异步请求的源码分析中,有这么一段
那么OkHttp对线程池又做了怎样的使用呢?目的是什么?
在分发器中,找到了创建线程池的方法,可以发现两个疑点
第一个是核心线程数为0,最大线程数为Integer.MAX_VALUE
第二个是阻塞队列采用的是SynchronousQueue
为什么这样用呢?
首先看下线程池的三种阻塞队列
①ArrayBlockingQueue
看下面一段代码。
首先创建阻塞队列,容量为1,然后创建核心线程数为0,最大线程数为Integer.MAX_VALUE,且阻塞队列为ArrayBlockingQueue
的线程池.。然后分别执行两个任务,第一个任务是死循环,第二个任务是一个输出任务。
这段程序运行的结果为:
只输出了任务1
原因:看线程池的源码
按线程池的执行逻辑,因为核心线程为0,所以会先把任务1加到阻塞队列,然后再判断,当前work的线程为0,所以要创建一个线程,去执行任务1,因为任务1是死循环,所以就跳不出来了。所以任务2会一直待在阻塞队列中,得不到执行。当再来一个任务的时候,即任务三,会出现什么情况呢?
会发现任务1仍然会阻塞,但会先执行任务3,再执行任务2,且任务3和任务2在同一个线程中执行。
原因:因为线程3来的时候,阻塞队列已满,所以又会创建一个新线程,先执行任务三,然后再把阻塞队列当中的任务执行完。
这个阻塞队列,必须得初始化容量
②LinkedBlockingQueue
这个阻塞队列和第一个阻塞队列的底层区别就是,这个阻塞队列是链表。
还有一个区别是,LinkedBlockingQueue
可以不初始化容量,当没有初始化的时候,容量就是Integer.MAX_VALUE,所以如果还是上面的代码,加上任务3之后,任务3和任务2都不会执行,都会加入到阻塞队列当中去。
③SynchronousQueue
这个阻塞队列,无容量。这样再配合核心线程数为0,就能达到最大并发量的效果。来一个任务,就能立马执行,而无需等待。当然也不会真的达到Integer.MAX_VALUE,因为有64的限制
使用线程池,就是为了线程复用。当然如果使用前两个阻塞队列,把容量改为0,好像貌似也能达到相同的效果?
解决问题
之所以这么用线程池,就是为了达到无等待,最大并发的效果