介绍
OkHttp 现在作为Android时下最流行的网络框架(Retrofit 也是以OkHttp为底层的).先将okhttp的学习整理如下,本文是以3.10版本进行分析.(因为3.9版本跟3.10版本差别不大,有些图片的引用还是3.9的)
感谢: json_it的博客 码迷
okhttp网络请求流程
以官网demo为例进行分析
Get请求1 | OkHttpClient client = new OkHttpClient(); |
1 | public static final MediaType JSON |
调用流程图如下
类,方法具体分析
OkHttpClient
要完成一次请求,我们必须先有OkHttpClient实例,使用者的所有操作都是通过它来完成的.OkHttpClient用于协调各个子模块工作,各个子模块直接相互独立.OkHttpClient因为有众多子模块,所以使用建造者设计模式进行装配.获取OkHttpClient实例有两种方式
方式1:
1 | OkHttpClient client=new OkHttpClient(); |
不是说使用的是建造者模式吗?为啥没有Builder?
因为OkHttpClient的构造函数默认使用了默认的builder()
1 | public OkHttpClient() { |
方式2:
1 | OkHttpClient.Builder bulider=new OKHttpClient.Builder(); |
注: 因为OkHttpClient内部有连接池,线程池等,所以一般来说一个应用就一个OkHttpClient实例.
Request
Request类比较简单 只包含了 url, method, headers, body等一次请求的基本信息
RealCall
RealCall 目前是Call的唯一实现类,表示一个请求/响应对(流),用于操作管理请求.如:发送同步/异步请求,取消请求等.每一个call只能执行一次.其中重要方法如下
同步请求:
异步请求:
getResponseWithInterceptorChain:
从以上可以看到,RealCall 通过 execute / enqueue发送同步/异步请求,
getResponseWithInterceptorChain获取Response.其中异步请求会用Dispatcher来进行分配,虽然同步请求中也使用了Dispatcher但只是记录,稍后会在Dispatcher中详细分析.在getResponseWithInterceptorChain中,将开发者自定义的interceptor可必须的interceptor为每一个call进行添加,然后通过RealInterceptorChain进行逐个调用,具体会在下面分析.
Dispatcher
Dispatcher保存了一个OkHttpClient里的所有Call,包含同步Call(一个deque保存)和异步Call(两个Deque保存).其中异步Call默认最大支持64请求,单个Host默认最大5个请求.每个dispatcher使用一个{@link ExecutorService}来在内部运行调用。默认使用ExecutorService实现类ThreadPoolExecutor.如图:
同步请求:
在上面RealCall同步请求分析中,需要Dispatch做什么呢?
RealCall
1 | public Response execute() throws IOException { |
Dispatcher
1 | ....... |
可以看到在同步请求中,Dispatcher只是将任务存入了runningSyncCalls集合中.在通过getResponseWithInterceptorChain获取到Response,最终会执行finally语句,将任务移除runningSyncCalls集合.
异步请求:
在RealCall执行异步请求时,最关键的代码是:
1 | public void enqueue(Callback responseCallback) { |
Dispatcher中enqueue方法
1 | synchronized void enqueue(AsyncCall call) { |
可以看到如果正在执行的异步请求数小于最大请求数并且单个host异步请求数小于 maxRequestsPerHost(5)时,将AsyncCall直接放入正在执行的异步集合中,并立即执行,否则就将该请求放入readyAsyncCalls集合中.AsyncCall是Runnable的子类(间接),因此,在线程池中最终会调用AsyncCall的execute()方法执行异步请求.
1 | protected void execute() { |
异步请求,最终也是通过getResponseWithInterceptorChain来获取响应的Response. 区别在于client.dispatcher().finished(this);因为这是另一个异步任务,所以调用的是另外一个finish方法:
1 | void finished(AsyncCall call) { |
因为promoteCalls为true,所以要执行promoteCalls(); 方法:
1 | private void promoteCalls() { |
可以看到如果有等待的异步请求,则会在符合最大请求数以及最大单个host请求数的前提下,继续执行.直到所有请求都执行完毕.
RealInterceptorChain
在介绍各个拦截器之前,需要先分析RealInterceptorChain,直译就是拦截器链.为什么要先分析这个类呢?因为各个Interceptor就是通过RealInterceptorChain这个类按照顺序调用的!,先看这个类是在哪里调用的,在上面分析RealCall中获取Response的getResponseWithInterceptorChain方法中:
可以看到我们添加的所有interceptors都传给了RealInterceptorChain类中,并通过RealInterceptorChain的proceed方法获取Response.我们在来看proceed方法:
1 | public Response proceed(Request request) throws IOException { |
这个方法最重要的就是又重新创建了一个RealInterceptorChain(此时index+1),并且通过interceptors.get(index);获取拦截器后执行拦截器intercept方法.在后续分析中可以看到intercept(Chain chain)方法会继续调用责任链的proceed()方法.这样依次调用,当前拦截器的Response依赖于下一个拦截器的Intercept的Response。当执行到最后一个拦截器获取到Response后沿着调用相反的方向依次将Response返回,有点类似递归.
RetryAndFollowUpInterceptor
根据类名我就可以知道此拦截器用于失败重试,并根据需要进行重定向,并且如果调用被取消它可能会抛出IOException(“Canceled”)。
具体方法如下:
1 | public Response intercept(Chain chain) throws IOException { |
BridgeInterceptor
从应用程序代码到网络代码的桥梁。首先,它从用户请求构建网络请求。然后它继续调用网络。最后,它从网络响应构建用户响应。其实就是对Request和Response的Header进行操作,主要是为Request添加请求头,为Response添加响应头如图:
功能很简单,就不贴源码了
CacheInterceptor
cache 缓存,CacheInterceptor就是用来处理缓存的拦截器.简单来说一次请求中,如果有缓存的Response则直接返回,如果没有,就在拦截器链返回Response后缓存起来.
在解析CacheInterceptor之前,我们要了解HTTP缓存机制(HTTP缓存机制-客户端缓存).HTTP缓存机制图片:
缓存响应头:
Cache-control:在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。 HTTP-Header中的Cache-Control字段:可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age
各个消息中的指令含义如下:
- public指示响应可被任何缓存区缓存。
- private指示对于单个用户的整个或部分响应消息,不能为共享缓存处理.这允许服务器仅仅描述当前用户的部分消息,次响应消息对其他用户的请求无效
- no-cache指示请求或响应消息不能缓存
- no-store用于防止重要的信息被无意发布.在请求消息中发送将使得请求和响应消息都不使用缓存.
- max-age指示可以客户机接收生存期不大于指定时间(以秒为单位)的响应
- min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应.
- max-stale指示客户机可以接收超出超时期间的响应消息.如果指定max-stale消息的值,那么客户机可以接收超出超时期指定的值之内的响应消息
Date: 服务器告诉客户端,该资源的发送时间
Expires: 表示过期时间(该字段是1.0的东西,当cache-control和该字段同时存在的条件下,cache-control的优先级更高);
Last-Modified:服务器告诉客户端,资源的最后修改时间;
E-Tag:这个图没给出,意思是,当前资源在服务器的唯一标识,可用于判断资源的内容是否被修改了.
除以上响应头字段以外,还需了解两个相关的Request请求头:If-Modified-since、If-none-Match。这两个字段是和Last-Modified、E-Tag配合使用的。大致流程如下:
服务器在请求时,会在200 OK中返回该资源的Last-Modified和ETag头(服务器支持缓存的情况下才有),客户端将该资源保存在Catch中,并记录这两个属性.当客户端发送相同请求时,根据Date+Cache-control来判断是否缓存过期,如果过期,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器通过这两个头判断本地资源未发生变化,客户端不需要重新下载,返回304响应。
与ConnectInterceptor相关的几个类:
CacheStrategy:是一个缓存策略类,给定一个请求和缓存的响应,该数据将决定是否使用网络、缓存,或者两者都使用。
Cache是封装了实际的缓存操作;基于DiskLruCache
代码如下:
1 | public Response intercept(Chain chain) throws IOException { |
CacheStrategy
CacheStrategy在CacheInterceptor中起到了很关键的作用。该类决定了是网络请求还是使用缓存。该类最关键的代码是getCandidate()方法:
1 | private CacheStrategy getCandidate() { |
ConnectInterceptor
ConnectInterceptor是一个关于获取连接的拦截器.用于打开到目标服务器的连接并继续到下一个拦截器。ConnectInterceptor的intercept方法代码很简单,实际上大部分功能都被封装到其他类中.其中重要的几个类如下:
StreamAllocation:直译是流分配,用于协调链接,流,请求三者之间的关系.
RouteSelector:选择连接到源服务器的路由。每个连接都需要选择代理服务器、IP地址和TLS模式。连接也可以回收。
RouteDatabase: 创建到目标地址的新连接时要避免的失败路由的黑名单。这是为了让OkHttp能够从错误中吸取教训:如果尝试连接到特定的IP地址或代理服务器的失败,那么将记住失败,首选替代路由。
RealConnecton: Connect子类,主要实现连接的建立等工作
ConnectionPool: 连接池,用于实现连接复用.
HttpCodec: 编码HTTP请求并解码HTTP响应。针对不同的版本,OkHttp为我们提供了Http1Codec(Http1.x)和Http2Codec(Http2).
1 | public Response intercept(Chain chain) throws IOException { |
核心代码就这两行
1 | HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); |
可见主要的工作都是在StreamAllocation中完成的.StreamAllocation的newStream方法和connection方法做了什么呢?
newStream方法
1 | public HttpCodec newStream( |
可以看到在newStream方法中关键的方法是findHealthyConnection,此方法是找到一个可用的链接,如果不可用则会重复直到找到.
findHealthyConnection方法:
1 | private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, |
我们再看findConnection方法
1 | private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, |
ConnectionPool
在Connectinterceptor中,ConnectionPool起到关键作用.目前默认保存5个空闲链接,在5分钟不活动后将其清除。
线程池:用于支持连接池的cleanup任务,清除idle线程;
队列:存放待复用的连接;
路由记录表: 创建到目标地址的新连接时要避免的失败路由的黑名单。这是为了让OkHttp能够从错误中吸取教训:如果尝试连接到特定的IP地址或代理服务器的失败,那么将记住失败,首选替代路由。
对于连接池操作,关键就是 存,取,删除
存:
1 | void put(RealConnection connection) { |
在add(connection);之前,先进行了清除工作,cleanupRunnable中主要方法就是cleanup(System.nanoTime());
1 | long cleanup(long now) { |
取:
1 | RealConnection get(Address address, StreamAllocation streamAllocation, Route route) { |
首先,判断address对应的Connection是否还能承载一个新的StreamAllocation,如果可以得话,我们就将这个streamAllocation添加到connection.allocations中。最后返回这个Connection。我们再看isEligible方法
1 | public boolean isEligible(Address address, @Nullable Route route) { |
移除:
1 | List<RealConnection> evictedConnections = new ArrayList<>(); |
CallServerInterceptor
经过ConnectInterceptor后,此时链接connection和httpCodec,都已经准备好,现在就需要通过CallServerInterceptor真正的去请求服务器获取Response.
1 | public Response intercept(Chain chain) throws IOException { |
可以看到实际的IO操作都是由okio来进行的,这里不做过多的分析
总结
okhttp是一个Http+Htttp2客户端,适用于Android + Java 应用。其整体的架构如下:
(此图来源于https://yq.aliyun.com/articles/78105?spm=5176.100239.blogcont78104.10.FlPFWr,感谢)