谈谈OkHttp框架的原理(B站)
详细讲解
享学课堂移动互联网系统课程:设计思想解读开源框架《网络访问框架解析与封装实战》
基于OkHttp 版本:3.10.0
OkHttp介绍
OkHttp是当下Android使用最频繁的网络请求框架,由Square公司开源。Google在Android4.4以后开始将源码中的HttpURLConnection底层实现替换为OKHttp,同时现在流行的Retrofit框架底层同样是使用OKHttp的。
优点:
- 支持Http1、Http2、Quic以及WebSocket
- 连接池复用底层TCP(Socket),减少请求延时
- 无缝的支持GZIP减少数据流量
- 缓存响应数据减少重复的网络请求
- 请求失败自动重试主机的其他ip,自动重定向
- …….
使用流程
在使用OkHttp发起一次请求时,对于使用者最少存在OkHttpClient
、Request
与Call
三个角色。其中OkHttpClient
和Request
的创建可以使用它为我们提供的Builder
(建造者模式)。而Call
则是把Request
交给OkHttpClient
之后返回的一个已准备好执行的请求。
建造者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。实例化OKHttpClient和Request的时候,因为有太多的属性需要设置,而且开发者的需求组合千变万化,使用建造者模式可以让用户不需要关心这个类的内部细节,配置好后,建造者会帮助我们按部就班的初始化表示对象
同时OkHttp在设计时采用的门面模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。
OkHttpClient
中全是一些配置,比如代理的配置、ssl证书的配置等。而Call
本身是一个接口,我们获得的实现为:RealCall
Call
的execute
代表了同步请求,而enqueue
则代表异步请求。两者唯一区别在于一个会直接发起网络请求,而另一个使用OkHttp内置的线程池来进行。这就涉及到OkHttp的任务分发器。
分发器
Dispatcher
,分发器就是来调配请求任务的,内部会包含一个线程池。可以在创建OkHttpClient
时,传递我们自己定义的线程池来创建分发器。
这个Dispatcher中的成员有:
同步请求
因为同步请求不需要线程池,也不存在任何限制。所以分发器仅做一下记录。
异步请求
当正在执行的任务未超过最大限制64,同时runningCallsForHost(call) < maxRequestsPerHost
同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。
加入线程池直接执行没啥好说的,但是如果加入等待队列后,就需要等待有空闲名额才开始执行。因此每次执行完一个请求后,都会调用分发器的finished
方法
需要注意的是 只有异步任务才会存在限制与等待,所以在执行完了移除正在执行队列中的元素后,异步任务结束会执行promoteCalls()
。很显然这个方法肯定会重新调配请求。
在满足条件下,会把等待队列中的任务移动到runningAsyncCalls
并交给线程池执行。所以分发器到这里就完了。逻辑上还是非常简单的。
请求流程
用户是不需要直接操作任务分发器的,获得的RealCall
中就分别提供了execute
与enqueue
来开始同步请求或异步请求。
异步请求的后续同时是调用getResponseWithInterceptorChain()
来执行请求
如果该RealCall
已经执行过了,再次执行是不允许的。异步请求会把一个AsyncCall
提交给分发器。
AsyncCall
实际上是一个Runnable
的子类,使用线程启动一个Runnable
时会执行run
方法,在AsyncCall
中被重定向到execute
方法:
同时AsyncCall
也是RealCall
的普通内部类,这意味着它是持有外部类RealCall
的引用,可以获得直接调用外部类的方法。
可以看到无论是同步还是异步请求实际上真正执行请求的工作都在getResponseWithInterceptorChain()
中。这个方法就是整个OkHttp的核心:拦截器责任链。但是在介绍责任链之前,我们再来回顾一下线程池的基础知识。
分发器线程池
前面我们提过,分发器就是来调配请求任务的,内部会包含一个线程池。当异步请求时,会将请求任务交给线程池来执行。那分发器中默认的线程池是如何定义的呢?为什么要这么定义?
在OkHttp的分发器中的线程池定义如上,其实就和Executors.newCachedThreadPool()
创建的线程一样。首先核心线程为0,表示线程池不会一直为我们缓存线程,线程池中所有线程都是在60s内没有工作就会被回收。而最大线程Integer.MAX_VALUE
与等待队列SynchronousQueue
的组合能够得到最大的吞吐量。即当需要线程池执行任务时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。一般来说,等待队列BlockingQueue
有:ArrayBlockingQueue
、LinkedBlockingQueue
与SynchronousQueue
。
假设向线程池提交任务时,核心线程都被占用的情况下:
ArrayBlockingQueue
:基于数组的阻塞队列,初始化需要指定固定大小。
当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入队列就会失败,这时就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。所以最终可能出现后提交的任务先执行,而先提交的任务一直在等待。
LinkedBlockingQueue
:基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。
当指定大小后,行为就和ArrayBlockingQueu
一致。而如果未指定大小,则会使用默认的Integer.MAX_VALUE
作为队列大小。这时候就会出现线程池的最大线程数参数无用,因为无论如何,向线程池提交任务加入等待队列都会成功。最终意味着所有任务都是在核心线程执行。如果核心线程一直被占,那就一直等待。
SynchronousQueue
: 无容量的队列。
使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合Integer.MAX_VALUE
就实现了真正的无等待。
但是需要注意的时,我们都知道,进程的内存是存在限制的,而每一个线程都需要分配一定的内存。所以线程并不能无限个数。那么当设置最大线程数为Integer.MAX_VALUE
时,OkHttp同时还有最大请求任务执行个数: 64的限制。这样即解决了这个问题同时也能获得最大吞吐。
拦截器责任链
OkHttp最核心的工作是在getResponseWithInterceptorChain()
中进行,在进入这个方法分析之前,我们先来了解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。
责任链顾名思义就是由一系列的负责者构成的一个链条,类似于工厂流水线,你们懂的,很多同学的男朋友/女朋友就是这么来的。
责任链模式
为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。比如:
七夕节刚过去。周周同学(我也不知道为什么第一个想到的就是周周同学)在读书的时候就是单身狗一条,看到自习室每天都很多美女后,每天晚上跑去自习都干同一件事情。
周周每天晚上都坐到自习室最后一排,找张纸条写上:“Hi,可以做我的女朋友吗?我的特长就是特别的长,如果不愿意请向前传”。纸条就一个接一个的传上去了,最后传给了扫地阿姨。最后和扫地阿姨过上了幸福的生活,这真是一个…令人高兴的故事。
那整个过程是什么样子的呢?
在责任链模式中,每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的 情况下动态的重新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。