为了更好的使用OKHttp—架构与源码分析

今儿个咱们就来看看到底 okhttp 内部是如何实现的,这篇文章咱从 okhttp 整体框架方面出发,解析 okhttp源码

okhttp框架源码地址: github.com/square/okht…

如何使用 okhttp

OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().
        url("https://github.com/cozing").
        build();
Call call = client.newCall(request);
try {
    //1.同步请求方式
    Response response = call.execute();
    //2.异步请求方式
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            Log.info("cozing", "交易失败");
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            Log.info("cozing", "交易成功");
        }
    });
} catch (IOException e) {
    e.printStackTrace();
}
复制代码

这是使用okhttp发送一个简单通信流程,其中包括同步请求和异步请求:

  1. 同步请求调用的方法是 call.execute() ,内部采用的是线程阻塞方式直接将结果返回到Response,后面咱们会详细讲解;
  2. 异步请求调用的方法是 call.enqueue(Callback callback) ,该方法需要传入一个Callback等待结果回调的接口,交易结果在 onFailure()(失败)onResponse()(成功) 中返回。

那么,接下来咱们来看看okhttp的源码的整体流程。

整体架构流程图

接下来咱们将根据这个整体架构图来来看看 okhttp 的内部实现。

为了更好的使用OKHttp—架构与源码分析

流程走读

创建OkHttpClient

首先创建 OkHttpClient 对象, OkHttpClient 是okhttp框架的客户端,用于发送http请求(Requests)和读取交易返回数据(Responses)。官方建议使用单例创建 OkHttpClient ,即一个进程中只创建一次即可,以后的每次交易都使用该实例发送交易。这是因为 OkHttpClient 拥有自己的连接池线程池,这些连接池和线程池可以重复使用,这样做利于减少延迟和节省内存,如果咱们每次发交易都创建一个 OkHttpClient 的话,将会浪费很多内存资源。

创建方式:

OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().
        url("https://github.com/cozing").
        build();
Call call = client.newCall(request);
复制代码

还有一种方式创建OkHttpCleint对象的方式:

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().
        url("https://github.com/cozing").
        build();
Call call = client.newCall(request);
复制代码

这两种创建方式内部实现,第一种采用构建者模式创建 OkHttpClient 对象,这种方式可以自定义 Builder 内部的每一个参数属性,第二种采用普通的创建实例方式创建一个 OkHttpClient 对象,内部创建了一个默认的 Builder ,因此 这种方式使用默认Builder的内部属性。

创建Call对象

一个 Call 对象表示一次请求,每一次交易请求都会生产一个新的 CallCall 其实是一个接口对象,它的具体实现类是 RealCall

Call 的创建过程:

Request request = new Request.Builder().
        url("https://github.com/cozing").
        build();
Call call = client.newCall(request);
复制代码

可以看到在创建Call对象时候传进去了一个 Request 对象, Request 对象表示用户的交易请求参数,咱们看看它的内部实现:

为了更好的使用OKHttp—架构与源码分析

可以看到,里面包括:

  • url:交易请求地址;
  • method:请求方式,比如:get/post/http…等请求方式;
  • headers:请求头;
  • body:请求体;

咱们这时看看 Call 的创建方法 client.newCall(request) 的内部实现:

为了更好的使用OKHttp—架构与源码分析

继续看
RealCall()

为了更好的使用OKHttp—架构与源码分析

可以看到确实如上面咱们所说,最后创建的是
RealCall

对象。

请求交易

okhttp中提供了两种请求方式:一种是同步请求,第二种是异步请求。同步请求调用 call.execute() 方法,异步请求调用 call.enqueue(Callback callback) 方法,

在看两个请求方式的实现之前,咱们先来看okhttp中一个重要成员 Dispatcher(调度器)

Dispatcher(调度器)

Dispatcher 是okhttp的任务调度核心类,负责管理同步和异步的请求,管理每一个请求任务的请求状态,并且其内部维护了一个线程池用于执行相应的请求, Dispatcher 的实现框架图:

为了更好的使用OKHttp—架构与源码分析

Dispatcher内部维护了两个队列:

为了更好的使用OKHttp—架构与源码分析

两个队列的作用可以看上面注释,那么为何要使用这两个队列呢?咱们可以这么理解:把
Dispatcher 当成生产者,把线程池当成消费者,当生产者生产的线程大于消费者所能承受的最大范围,就把未能及时执行的任务保存在
readyAsyncCalls 队列中,当时机成熟,也就是线程池有空余线程可以执行时,会调用
promoteCall() 这个方法把等待队列中的任务取出放到线程池中执行,并且把这个任务转移到
runningAsyncCalls

队列中去。

接下来咱们分别看看同步请求和异步请求的实现过程,并详细说一下他们是如何实现的。

同步交易请求

call.execute() 实现方式:

为了更好的使用OKHttp—架构与源码分析
为了更好的使用OKHttp—架构与源码分析

可以看到实际调用的是
Dispatcher 调度器的
executed()

方法,继续看Dispatcher的实现:

为了更好的使用OKHttp—架构与源码分析

到此咱们可以知道这部分是将这次交易的请求
RealCall 存进了
Deque 队列,
Deque 是一个双向队列接口,
Deque

接口具有丰富的抽象数据形式,它支持从队列两端点检索和插入元素,在此不对其做过多讲解。

接下来看 的client.dispatcher().finished(this) ,不管结果请求结果如何,都会调用 finally 中的 client.dispatcher().finished(this) 将本次请求从队列中移除。

接下来调用到 getResponseWithInterceptorChain() 方法:

为了更好的使用OKHttp—架构与源码分析

这个方法是okhttp的实现精髓点之一,这部分咱先放一边,将会在异步请求中一起讲解。

异步交易请求

call.enqueue(Callback callback)实现方式:

为了更好的使用OKHttp—架构与源码分析

当调用此次异步请求方法的时候,内部是调用了调度器
dispatcher
equeue(new AsyncCall(responseCallback)) 方法,该方法需要传入一个
AsyncCall

的对象。

接下来看dispatcher的equeue(new AsyncCall(responseCallback))方法的实现:

为了更好的使用OKHttp—架构与源码分析

先判断当前运行中的请求数是否小于设定的最大请求数量,默认最大请求数是同时执行64个请求,并且判断当前运行中的共同主机的请求数量是否小于设定的最大请求数量,默认同一主机的请求数量最大值为5,当两者条件都成立的时候会调用
executorService()
execute(call) 方法;两者中只要有一个条件不成立,就会调用
redyAsncCalls.add(call) 将表示此次请求的
call 对象存在
readyAsyncCalls 队列中,
readyAsyncCalls

表示已准备好并等待执行请求的队列,当有空闲网络请求线程时,会从该队列中取出并执行网络请求。

接下来看 executorService().execute(call)

为了更好的使用OKHttp—架构与源码分析

可以看到调度器Dispatcher内部维护了一个ThreadPoolExecutor线程池,并直接将call对象传入线程池执行。

通过上图可以知道这个 call 的实现对象类型是 AsyncCall ,来看看内部实现:

为了更好的使用OKHttp—架构与源码分析

可以看到
AsyncCall
RealCall 的一个内部类,继承自
NamedRunnable ,再看
NamedRunnable

为了更好的使用OKHttp—架构与源码分析

所以,
AsyncCall 就是一个
Runnable 的实现,用来开启一个线程,当网络请求线程池执行该线程的
run() 方法时,会调用
AsyncCall
execute() 的方法,最后在
execute() 方法内部调用了和上面咱们分析的同步请求方法一样的
getResponseWithInterceptorChain()

getResponseWithInterceptorChain()/拦截器链

通过上面的分析咱们知道不管是同步请求还是异步请求,最后都会走 getResponseWithInterceptorChain() 方法, getResponseWithInterceptorChain() 是okhttp中的精髓设计之一,那么现在咱们来看看这个方法的内部实现:

为了更好的使用OKHttp—架构与源码分析

这个方法是通过拦截器链对请求数据和返回数据进行处理,内部采用责任链模式,将每一个拦截器对应负责的处理任务进行严格分配,最后将交易结果返回并回调暴露给调用者的接口上。

这些拦截器包括:

  1. 用户自定义的拦截器
  2. retryAndFollowUpInterceptor :重试和重定向拦截器,主要负责网络失败重连。
  3. BridgeInterceptor :主要负责添加交易请求头。
  4. CacheInterceptor缓存拦截器,主要负责拦截缓存。
  5. ConnectInterceptor :网络连接拦截器,主要负责正式开启http请求。
  6. CallServerInterceptor :负责发送网络请求和读取网络响应。

有关每个拦截器的具体实现和内部流程,读者可自行阅读源码了解,这篇文章咱们主要还是分析okhttp的整体架构。

根据上面的源码可以看到,最后交给了 RealInterceptorChain(拦截器链类) 这个真正的处理类,并调用 RealInterceptorChain``的 proceed()`方法来实现,具体实现:

为了更好的使用OKHttp—架构与源码分析
为了更好的使用OKHttp—架构与源码分析

会调用拦截器的
intercept(next) 方法,只有当前拦截器的
response 返回有结果时,才会执行下一个拦截器,因此得出结论:
下一个拦截器依赖于当前拦截器的返回,可以保证拦截器的依次执行。

在拦截器链中执行的结果,在同步请求中会直接在response返回,而异步请求:

为了更好的使用OKHttp—架构与源码分析

异步请求时会把拦截器链的处理结果通过
Callback
onReponse

回调给用户。

原文 

https://juejin.im/post/5df0bbbbe51d45581e4404cf

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » 为了更好的使用OKHttp—架构与源码分析

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址