本文OkHttp源码基于3.14.x,版本下载地址:okHttp 3.14.x
前言
OkHttp是一个非常优秀的网络请求框架,使用方便,操作简单,并且目前比较流行的Retrofit也是默认使用OkHttp。因此从源码深入理解OkHttp是非常有必要的。故今天这篇首先将介绍OkHttp请求的执行流程。另外由于OkHttp从4.x版本开始使用Kotlin来编写,因此今天的源码解析是基于Java版的3.14.x版本。
一、基本使用
既然是开源库,第一步当然先得添加OkHttp的依赖,另外注意得在清单文件声明网络权限。
1 | implementation 'com.squareup.okhttp3:okhttp:3.14.0' |
我们知道Http常用的Http请求有Get和Post,在这里我们只以Get请求为例子进行分析。
1. 同步GET请求
1 | //在这里也可以通过okHttpClient的new操作来创建OkHttpClient实例 |
2. 异步GET请求
1 | //在这里也可以通过okHttpClient的new操作来创建OkHttpClient实例 |
比较同步GET请求和异步GET请求,你会发现其实两者区别并不是很大,主要就是在最后一步提交请求的方式不同,故OkHttp的GET请求的流程可以总结为:
- 构造OkhttpClient对象,可以通过new操作直接构造或者通过构造OkhttpClient内部类Builder对象间接构造
- 构造Request对象
- 构造Call对象
- 提交请求,获取响应数据Response。如果是同步请求,则通过Call对象的execute方式提交请求;如果是异步请求,则通过Call对象的enqueue方式提交请求
下面我们将基于源码来分析这4步操作!
二、流程
1. 构造OkHttpClient对象,配置属性
通过上面的分析,我们知道构造OkHttpClient对象的方式有两种,由于直接构造是基于间接构造的基础上,所以我们首先看看间接方式是怎么构造这个OkHttpClient对象的!
1.1 间接构造
通过上面基本方式的介绍,我们知道首先会构造Builder对象,这个Builder是OkHttpClient类的内部类
OkHttpClient.Builder#构造器
1 | public static final class Builder { |
通过注释我们可以知道Builder的构造器中初始化了一系列重要参数的默认值,比如调度器Dispatcher。也可以在里面找到我们熟悉的连接超时时间,读取超时时间,写入超时时间值的初始化。那我们要修改这些默认值该怎么办呢?没错,Builder还提供了一系列方法供我们修改默认值
OkHttpClient.Builder
1 | public Builder readTimeout(long timeout, TimeUnit unit) { |
最后再通过Builder的build来构造OkHttpClient对象
OkHttpClient.Builder#build
1 | public OkHttpClient build() { |
可以发现其实一系列下来就是利用Builder设计模式来构造OkHttpClient对象,然后我们来看看OkHttpClient又做了哪些事
OkHttpClient#构造器
1 | //设置默认值 |
OkHttpClient做的事情很简单,就是将Builder对象的值赋值到自己的成员变量中,于是OkHttpClient对象构造成功。
1.2 直接构造
直接构造只需一行代码
OkHttpClient okHttpClient = new OkHttpClient();
我们继续看下OkHttpClient的构造器
OkHttpClient#构造器
1 | public OkHttpClient() { |
看到这里是否突然明白,原来直接构造也是构造了Builder对象的,然后再将Builder对象的默认值赋值给OkHttpClient对象的成员变量中,这么一看好像直接构造和间接构造好像也没什么区别啊?
真的是这样吗,其实并不是,细看的话你就会发现通过直接构造只能构造出默认值,而通过间接构造就可以修改默认值。并且如果想添加拦截器的话,就必须得通过间接构造。
1.3 小结
构造OkHttpClient对象有两种方式,两者的使用场景如下:
- 如果直接使用默认值的话,可以直接使用new操作来构造OkHttpClient对象。
- 如果想自行设置其中的属性,比如连接超时时间或者添加拦截器的话,必须得首先构造Builder对象,然后设置相关的属性,最后调用Builder对象的build来构造OkHttpClient对象。
2. 构造Request对象,配置属性
Request对象的构造也是通过Builder模式来构造的,代码来说也是比较容易看懂的。我们首先看Request中的Builder的构造方法
Request.Builder#构造方法
1 | public Builder() { |
可以发现在Builder构造中已经设置了GET的请求方法了,所以如果是GET请求的话后面其实可以不需要自己设置了。然后我们还需要设置请求地址。
Request.Builder#url
1 | /** |
设置完请求地址后,我们需要通过build方法来构造Request对象
Request.Builder#build
1 | public Request build() { |
Request#构造方法
1 | Request(Builder builder) { |
在Request的构造器中将Builder的值赋值到自己的成员变量中,于是Request构造成功。
3. 构造Call对象
通过基本使用我们知道Call对象会调用OkHttpClient对象的newCall来构造
OkHttpClient#newCall
1 | public Call newCall(Request request) { |
在这个方法中,其实主要目的是将前面构造的OkHttpClient对象和Request对象传给RealCall的newRealCall方法中
RealCall#newRealCall
1 | static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) { |
而这个newRealCall方法也是很简单,就是构造了RealCall对象,RealCall对象保存了OkHttpClient对象和Request对象,所以最后构造出来的Call对象其实就是RealCall对象。
4.提交请求, 获取响应数据Response
终于来到执行流程的重头戏了,我们首先分析同步请求的提交。
4.1 同步
同步请求是调用了Call对象的execute来提交请求,不过需注意的是这种方式会阻塞线程,所以在使用的时候应该开启子线程然后再调用execute,否则就会引起ANR。通过上面的分析我们已经知道Call对象其实是RealCall对象,所以会调用RealCall对象的execute方法来提交请求。
RealCall#execute
1 | // 同步Call,一个Call只能被执行一次 |
仔细观察execute方法,你就会发现其实这个方法主要做了三件事:
- 将请求加入到同步任务的执行队列
- 调用各拦截器对请求进行处理,获取响应数据
- 关闭当前请求任务
我们接下来将根据这3件事来进行分析!
1. 将请求加入到同步任务的执行队列
在execute方法中通过一行代码来实现这个任务
client.dispatcher().executed(this);
此时的client此时就是OkHttpClient对象,dispatcher就是在构造OkHttpClient对象时初始化的Dispatcher对象,所以我们接下来需要看Dispatcher的executed方法
Dispatcher#executed
1 | //同步任务的执行队列 |
在这里runningSyncCalls是ArrayDeque对象,ArrayDeque既可以作为栈使用,效率高于Stack,又可以作为队列使用,效率也比LinkedList更好一点,在这里就是当作双端队列来使用,所以runningSyncCalls就是同步任务的执行队列,而Dispatcher的executed方法也很简单,就是简单的将同步请求添加到执行队列的队尾。
2. 调用各拦截器对请求进行处理,获取响应数据
下列只介绍拦截器链的大概调用流程。由于拦截器链算的上是整个框架的精髓,考虑到篇幅问题,对拦截器链的详细介绍将会在下一篇文章进行分析。
我们回到RealCall的execute方法,通过调用getResponseWithInterceptorChain来获取响应数据
RealCall#getResponseWithInterceptorChain
1 | Response getResponseWithInterceptorChain() throws IOException { |
在这个方法中,首先会添加一系列的拦截器,按添加的顺序分别是:
- 构造okHttpClient对象时通过addInterceptor配置的全局拦截器
- 错误,重定向拦截器
- 桥接拦截器,桥接应用层和网络层,添加必要的头
- 缓存拦截器,从缓存中拿数据
- 网络拦截器,建立网络连接
- 构造okhttpClient对象时通过addNetworkInterceptor配置的非网页请求拦截器
- 服务器请求拦截器,向服务器发起请求获取数据
可以看出各拦截器各司其职,由于在这里使用的是类似责任链的设计模式,所以在这里需要特别注意添加的顺序,因为添加的顺序就是执行拦截器的顺序。
然后接着构造了拦截器链的头结点,这里需注意其中的第4个参数为0,即该结点对应了0位置的拦截器。
最后调用了该结点的proceed方法来处理该请求。让我们看看这个proceed方法
RealInterceptorChain#proceed
1 | public Response proceed(Request request) throws IOException { |
这个方法首先构造了头结点的下一个结点,其位置为index+1。然后根据index和拦截器列表拿到当前位置的拦截器,最后再调用当前拦截器的intercept方法来处理请求,这里还需特别注意的是再处理请求时也将拦截器链的下一节点传进去了。接着就是责任链模式的传递过程,在这里我们假设构造OkHttpClient没有添加全局拦截器,所以根据上面的分析,头结点的拦截器应该是RetryAndFollowUpInterceptor,错误重连拦截器。
RetryAndFollowUpInterceptor#intercept
1 | public Response intercept(Chain chain) throws IOException { |
这个拦截器的详细工作会在下篇文章OkHttp 3.14.x 源码解析-拦截器中进行分析,这里我们只分析大概的执行流程。可以发现这个拦截器还会调用下一结点的proceed方法,这样就可以实现递归遍历在一开始添加的拦截器列表中Interceptor中的intercept方法。当遍历到最后一个服务器请求拦截器时,会直接返回Response,然后又将这个Response一直向上返回,最后返回拦截器链处理过后的响应数据。
3. 关闭当前请求任务
不管有没有得到响应数据,最后我们都得关闭当前的请求任务,我们继续看回RealCall的execute方法
1 | public Response execute() throws IOException { |
这时候会调用Dispathcer的finished方法来关闭当前请求任务,需注意this为RealCall对象
Dispatcher#finished
1 | //同步请求 |
从finished方法可以看出,其实关闭当前请求任务就是将该请求从执行队列中移除,而promoteAndExecute方法其实是异步任务完成后处理,同步任务执行promoteAndExecute方法是没有效果的。
到这里同步获取响应数据分析完毕!接着让我们继续分析异步请求是如何获取响应数据的。
4.2 异步
异步是通过ReaCall的enqueue来提交请求的,我们看看enqueue方法
RealCall#enqueue
1 | //异步call,一个call只能被执行一次 |
这里的responseCallback其实就是我们在使用异步请求时的匿名Callback类的回调对象,在这里根据Callback回调接口对象构造了AsyncCall对象,然后调用了Dispatcher的enqueue方法
Dispathcer#enqueue
1 | //异步任务的就绪队列 |
跟同步请求一样,这里的readyAsyncCalls也是双端队列,不过readyAsyncCalls并不是执行队列,而是异步任务的就绪队列,然后调用promoteAndExecute方法
Dispathcer#promoteAndExecute
1 | private int maxRequests = 64;//允许执行的最大请求数 |
这个方法首先要遍历就绪队列,OkHttp对执行任务的数量做了要求:
- 当前正在执行的异步任务不能超过64个
- 一个主机正在执行的异步任务不能超过5个
如果满足上述条件的话,就将当前请求从异步任务的就绪队列中移除,并且添加到异步任务的集合和异步任务的执行队列中。接着就继续遍历当前要执行的异步任务,然后执行异步任务。
在执行异步任务时可以发现这里调用了executorService方法。我们来瞧瞧
Dispatcher#executorService
1 | public synchronized ExecutorService executorService() { |
这个方法也挺简单的,就是返回了一个执行线程池的对象,接着就会调用AsyncCall的executeOn方法
RealCall.AsyncCall#executeOn
1 | void executeOn(ExecutorService executorService) { |
这个方法就是调用了刚传进去的执行线程池的对象来执行任务,然后就会执行AsyncCall的run方法。然后你就会发现AsyncCall中根本没有run方法,想必机智的你应该想到下一步应该怎么做了,没错!在AsyncCall父类找这个方法
NamedRunnable
1 | final class AsyncCall extends NamedRunnable { |
果然如此,但是在NamedRunnable的run方法中又会重新调用AsyncCall的execute方法,兜兜转转,又回到了AsyncCall中
AsyncCall#execute
1 | protected void execute() { |
仔细看这个方法,你会发现这个方法其实主要工作有三件:
- 调用各拦截器对请求进行处理,获取响应数据
- 将请求结果回调给调用层
- 关闭当前请求任务
按照老套路,我们将对这三件工作逐个分析。
1. 调用各拦截器对请求进行处理,获取响应数据
这个分析与同步请求时的分析是一样,这里不再进行分析,忘记的小伙伴可以向上翻翻
2. 将请求结果回调给调用层
这里的回调仍然是在子线程中,在实际使用时需要回到主线程对响应数据进行操作。
当获取到响应数据时,会调用responseCallback的onResponse将响应数据回调出去,这里的responseCallback就是我们在实际使用时用匿名内部类的构造的Callback对象,通过回调就能执行Callback中的onResponse方法。如果捕获到异常就回调Callback的onFailure方法。
3. 关闭当前请求任务
不管请求结果是否成功,都要关闭当前的请求任务,这里跟同步关闭请求任务一样,调用了Dispatcher的finished,不过注意参数不一样,这里传递的参数为AsyncCall对象
Dispatcher#finished
1 | //异步请求 |
首先还是得将请求完任务从异步任务的执行队列中移除,然后还需要判断是否有需要执行的任务,这里又会调用promoteAndExecute方法,这个方法是否似曾相识呢?其实这个方法已经在上面分析过了,就是接着执行下一个异步任务。
4.3 小结
同步和异步的GET请求的区别就在于提交请求这一流程不同,两者提交请求的不同之处在于:
- 异步提交请求比同步多了一个就绪队列,原因是异步一次能够执行多个任务,而同步一次只能执行一个任务
- 关闭当前请求任务时,同步只是将当前请求任务从同步任务的执行队列中删除,而异步将请求从异步任务的执行队列中移除后,还会遍历就绪队列来执行下一次任务。
总结
到这里OkHttp的同步和异步请求的执行流程就分析完毕了。我们可以稍微歇会,然后一鼓作气,继续探索OkHttp的神秘王国-OkHttp 3.14.x 源码解析-拦截器!
参考文章: