引子
OkHttp 知名第三方网络框架SDK,使用简单,性能优秀,但是内核并不简单,此系列文章,专挑硬核知识点详细讲解. 何为硬核,就是要想深入研究,你绝对绕不过去的知识点
正文大纲
OkHttp是什么OkHttp怎么用OkHttp源码核心类之一:分发器详解OkHttp源码核心类之一:拦截器简述
正文
OkHttp是什么
OkHttp是时下非常流行的网络编程框架,由行业巨佬Square公司开源,很多其他的流行框架比如retrofit的底层也是okhttp,只不过使用了注解反射动态代理将其进行了封装。流行版本为:3.10.0,最新版本为:4.0.1,只不过将实现语言从java改成了kotlin。
相对于其他网络框架,有如下优点:
支持Spdy、Http1.X、Http2、Quic以及WebSocket连接池复用底层TCP(Socket),减少请求延时无缝的支持GZIP减少数据流量缓存响应数据减少重复的网络请求请求失败自动重试主机的其他ip,自动重定向
OkHttp怎么用
添加gradle依赖
dependencies {
....
implementation("com.squareup.okhttp3:okhttp:4.0.1")
}
Java调用(同步请求,异步请求)
public class MyRequest {
/**
* 异步请求
*/
public void sendReqAsync() {
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url("http://www.baidu.com").build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d("sendReqTag", "onFailure:" + e.getLocalizedMessage());
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
String s = new String().concat(response.code() + "n")
.concat(response.message() + "n")
.concat(response.body().string());
Log.d("sendReqTag", "onSuccessn " + s);
}
});
}
/**
* 同步请求
*/
public void sendReqSync() {
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url("http://www.baidu.com").build();
Call call = client.newCall(request);
try {
Response response = call.execute();
String s = new String().concat(response.code() + "n")
.concat(response.message() + "n")
.concat(response.body().string());
Log.d("sendReqTag", "onSuccessn " + s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
OkHttp的简单使用方法大致使用如上,其中也有一些细节需要注意:1、在应用层使用OkHttp,必然会涉及到4个重要元素:OkHttpClient类(产生OkHttp客户端实例)Request类(请求封装)Call类(网络任务封装,并决定是要同步执行还是异步执行,注意,同步请求不可以放在主线程中,但是异步请求可以 )Response类(网络任务执行之后的回调)2、执行网络请求必须在manifest中申请INTERNET权限,不然会抛异常.3、完整的一个请求执行出去,流程如下图:
OkHttp源码核心类之一:分发器详解
上述,提到Call类,可以选择性执行 同步或者异步请求,但是无论同步异步,都一定会经过一个门户:"分发器" :索引进源码(okhttp v3.10.0):
![37810f847773216a05babb19ccb225d4.png](https://i-blog.csdnimg.cn/blog_migrate/9ef5dc601fcaa0072c30d3da7851c8c5.png)
![b13889d9ff67eed1013c9897059e6d49.png](https://i-blog.csdnimg.cn/blog_migrate/812858b08b06870683a914e3c88fcdfc.png)
虽然用户不需要直接操作分发器,但是 分发器,作为OkHttp架构的一个门户层,是所有请求的必经之路,其中的代码还是有必要了解细节的。
同步请求
进入分发器 Dispatcher之后, 会执行 getResponseWithInterceptorChain() 来执行这个Call任务,得到一个Response,其中的细节分为两步:
1、client.dispatcher().executed(this); ,进入源码可以看到 仅仅是执行了 runningSyncCalls.add(call);,将call对象加入到了一个双端队列 Deque<RealCall> runningSyncCalls 中。2、 getResponseWithInterceptorChain() 是执行网络请求的核心内容,涉及到拦截器,在这一节上暂时不详述。
同步请求的执行步骤十分简单,将任务加入到 runningSyncCalls列表,并且直接执行核心方法,同步阻塞拿到response。
异步请求
异步请求进入分发器之后,
![434571923d32d4e926246f73ddf5d455.png](https://i-blog.csdnimg.cn/blog_migrate/eea5e629e79510701f9fe7ffec771bb7.jpeg)
可能会被加入到 Deque<AsyncCall> runningAsyncCalls 这么一个双端队列中,然后 executorService().execute(call);实际上是用了线程池来执行了这个异步任务。但是,请注意(还是刚才的enqueue方法代码)这里有一个判断条件 if分支 :
![b64f131f5347bb7ea1bd00ed85ebf389.png](https://i-blog.csdnimg.cn/blog_migrate/58c366cc12c66d8b1a0bd554d5442ce0.png)
这个条件是否满足,将会直接决定是直接执行这个任务,还是将任务加入到 readyAsyncCalls 双端队列.那么设置这个条件的目的是什么呢?从变量命名来看:runningAsyncCalls 执行中的异步任务runningCallsForHost 同一个域名正在执行的任务数readyAsyncCalls 预备执行的任务队列(尚未执行)当正在执行的任务数小于最大值(默认为64)并且,同一个域名正在请求的任务数小于最大值(默认5)时,才会立即执行,否则,这个任务会被加入到 readyAsyncCalls中等待安排。那么问题来了,readyAsyncCalls中的任务什么时候会被执行?追踪代码:追踪 readyAsyncCalls 的使用代码,找到遍历这个队列的地方:
![8ae610ba114fc2182ebc642f4d206eb4.png](https://i-blog.csdnimg.cn/blog_migrate/1baf59f585f737fd5c53ee5f37a1d160.png)
继续追踪,找到了这个 finish方法:
![da1368c496d0e3fd6c35ce2c53ec0bc8.png](https://i-blog.csdnimg.cn/blog_migrate/d79b4a4ce19b722eba8683266f5ec78e.png)
继续追踪finish在哪里调用的,找到两处:
![f219e63fb1c7b28b44c24c4e5b7ef16b.png](https://i-blog.csdnimg.cn/blog_migrate/c174cd7b26d4cd288f203a62e73ff72f.png)
![41ef27800b7e20b787fb6742c24c8350.png](https://i-blog.csdnimg.cn/blog_migrate/7d7bb739155166eea3b50e3050e5812d.png)
所以,得出结论: 在一个任务(无论同步还是异步)结束之后,分发器中的异步任务,存在两个队列,一个running队列,一个ready队列,当 running队列的size小于最大值,并且同一个域名正在执行的任务数小于最大值时,可以直接加入到running队列,立即执行。 如果不满足这条件,这个异步任务就会被加入到 ready队列.在任意一个任务(无论同步或是异步任务)执行完毕(无论成败)之后,就会遍历ready队列,每次从ready队列中取出一个任务,判断同时执行的异步任务数是否达到上限,并且同一主机的访问数是否达到上限,如果都满足,就加入到running队列,并且立即执行,不满足,就停止遍历。周而复始,直到所有的异步任务都执行完。
文字不够形象,画个图表示。
![26aad8515b2da1cc43b569942e2164ab.png](https://i-blog.csdnimg.cn/blog_migrate/993c40e6435e05f6c5d8aa5c91832614.jpeg)
关于okhttp的分发器Dispatcher用到的线程池
同步请求,没有用到线程池。
![a8ca64afccf215bb9291e7b116992ce7.png](https://i-blog.csdnimg.cn/blog_migrate/c84a906e10b0cfd0e7b9ac3439529f5f.jpeg)
但是异步请求的代码中,有这么一句。
![220e0a21d3c64cff55c84df96a0755da.png](https://i-blog.csdnimg.cn/blog_migrate/5b6bb16e43b0ff1ccd56195cce1336c7.jpeg)
我们知道,为什么这里会用到线程池呢?
1.观察 同步或者异步的call的实例。
![61d8819ce7b79b6f561f0ddd6b81fbf1.png](https://i-blog.csdnimg.cn/blog_migrate/f6aadd6d803e44baf34931728d7f3f41.jpeg)
那么这个Call是什么?它是一个接口,它的唯一实现类是RealCall,
![411abdc3def102ee2e13799c5bf12360.png](https://i-blog.csdnimg.cn/blog_migrate/f4f359760410abdbbc07e48e45a939b6.png)
在RealCall中,异步请求的执行方法,enqueue() 其实是交给了 分发器一个AsyncCall对象,它继承自NamedRunnable 可命名的Runnable任务。所以,这里可以用 线程池ExecutorService来执行这个Runnable.
进一步观察这个线程池的细节:
![83426c01ed8d57dc34f8f640e11ee30e.png](https://i-blog.csdnimg.cn/blog_migrate/59d0793ac86d459bded7d3e832ba006e.jpeg)
它是一个核心线程数为0的线程池,并且使用了一个无容量的阻塞队列作为参数。其实也不不必自己去创建线程池,而可以直接使用Executors.newCachedThreadPool(); 来创建,效果一样。线程池,系统提供了有多种默认实现
![11bcf54d74f2b19439d02a4fca597938.png](https://i-blog.csdnimg.cn/blog_migrate/8a1dc0705f835aed80ae87bbe36efc25.png)
为什么okhttp偏偏选择了这一种?
答:为了实现最大并发量。
详解如下:
既然这里提到了线程池,那么就把线程池的基本机制整理一下:
![a991fa790342a371945308eb54e980d3.png](https://i-blog.csdnimg.cn/blog_migrate/18566d1f0a20b43b34e0a0bb4e048c37.jpeg)
线程池的构造函数中,有一个阻塞队列参数。
![884c655e23a58ff82d70731f14361fc0.png](https://i-blog.csdnimg.cn/blog_migrate/1a864b6b79aeaf36093aef58da258ea7.png)
它有3个实现类:ArrayBlockingDeque/LinkedBlockingDeque / SynchronousQueue 是我们线程池经常用的。前面2个都是有容量的,而第三个是无容量的,加入进去,一定会失败。而参照上面线程池的工作流程图,如果加入失败,就会尝试去非核心线程执行任务。这样,便保证了每一个提交进来的异步任务,都会立即尝试去执行,而不是塞入等待队列中等待空闲线程,从而确保了 异步任务的并发。
OkHttp源码核心类之一:拦截器简述
上面讲解分发器的时候,提到了 RealCall类的getResponseWithInterceptorChain() 方法。它是一个网络请求执行的真正核心方法。
进入方法:
![1abdc6544ec6f1685061a7c1cc5cde02.png](https://i-blog.csdnimg.cn/blog_migrate/ebfb66f1f771eb5e35d7dd3303e5e5e3.png)
新建一个拦截器List,并且放入各种拦截器对象将拦截器list,交给RealInterceptorChain,进行责任链模式的调用,最终得出Response.
首先解释一下责任链模式,它是21种基本设计模式中,行为模式中一种。下面的案例可以很好地解释它:
![8fbe2ba4c03d32cfb590af1330be7985.png](https://i-blog.csdnimg.cn/blog_migrate/54f27a612858e379216af3c2a7bc892d.jpeg)
当一个国企要采购一批设备的时候,按照上图整个任务流程中,存在5个对象,都能对采购流程造成影响,采购任务开始的时候,是从上到下依次对采购流程负责。而总经理,他才不关心下面的人怎么操作,他只关心最后的结果。正如此案例中所述,okhttp的责任链模式,使用者也不需要关心这个请求到底经历了哪些过程,他只知道,我给了request,你就要给我response,而过程中,发生作用的各类拦截器,无需使用者知道,这样就达成了 面向对象程序开发中的最少知道原则。
而这些拦截器,恰恰是okhttp的核心内容,下篇文章将会详细讲解。
结语
本文是okhttp的开篇,如果要详细解读okhttp的每个细节,每一篇文章将会显得非常冗长而且乏味,所以我选了重要节点着重分析。 就像攻城略地打天下,先占领据点,再企图扩张,一步一个脚印,稳扎稳打,才能长远发展
你的赞和关注是我继续创作的动力~