OkHttp源码解析(一)- Dispatcher

2024-02-18 17:32

本文主要是介绍OkHttp源码解析(一)- Dispatcher,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

OkHttp的配置、使用步骤这里就不展开描述了,网络上有很多优秀的文章,这里主要是对学习源码中理解到的知识进行概括总结。

该文章根据OkHttp-3.11.0版本进行分析

首先介绍一下OkHttp的两种请求方式(不是get、post、put等等这些请求方式):

  • 请求方式
    1. 同步
      调用方式:call.execute
      阻塞线程

    2. 异步
      调用方式:call.enqueue
      不阻塞线程
      回调在子线程,不能做更新UI操作

再介绍一下OkHttp中核心的两个东西之一 - Dispatcher,也是这篇文章主要分析的东西:

  • Dispatcher

    意义:分发器,内部维护请求的状态(异步或同步),维护3个队列(ReadyAsyn:异步准备执行队列、RunningAsyn:异步执行中队列、RunningSyn:同步执行中队列),维护一个线程池(核心线程为0,最大线程数为Integer.MAX_VALUE,空闲时间60秒)。这里的线程池,虽然最大线程数定义为了Integer的最大值,但是在执行过程中会将执行的最大并发请求数默认设置为64个,所以

我们先来看看 Dispatcher 是如何构建出来的。
在我们使用OkHttp进行初始化的时候,是通过OkHttpClient的静态内部类 Builder 进行创建(这里使用了建造者模式,有兴趣的可以去看一下设计模式,OkHttp中大量运用了这种设计模式进行对象的创建),Builder 的构造方法中第一步就是创建了 Dispatcher 对象:

// OkHttpClient类
public Builder() {dispatcher = new Dispatcher();//省略其他代码
}//Dispatcher类
public Dispatcher(ExecutorService executorService) {this.executorService = executorService;
}public Dispatcher() {
}

可以看到,Dispatcher 的创建是调用了空参的构造方法,那么里面的线程池是什么时候创建的呢?
我们先看一下 Dispatcher 类中几个关键的属性:

  //并发最大请求数,正在执行的异步请求队列数超过最大请求数时,会把新的请求放在准备的异步请求队列private int maxRequests = 64;//同一Host的并发最大请求数,也就是说假设对于192.168.0.1 最多只能同时请求5次(未完成且未取消状态的请求),超过5个也会把新的同一Host的请求放入准备的异步请求队列private int maxRequestsPerHost = 5;private @Nullable Runnable idleCallback;//线程池,根据注释我们可以知道是用于执行异步请求的,并且是懒加载,也就是用到的时候再初始化/** Executes calls. Created lazily. */private @Nullable ExecutorService executorService;//准备的异步请求队列/** Ready async calls in the order they'll be run. */private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();//正在执行的异步请求队列,包含未完成的被取消的请求/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();//正在执行的同步请求队列,包含未完成的被取消的请求/** Running synchronous calls. Includes canceled calls that haven't finished yet. */private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

代码中我添加了中文注释,理解了这几个属性的含义对于接下来看源码的过程有帮助。代码注释中有说到线程池的创建是lazily的,用到再创建,那么我们从OkHttp开始请求时分析:

//同步
Response response = client.newCall(request).execute();//异步
client.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {}@Overridepublic void onResponse(Call call, Response response) throws IOException {}});

我们点到execute的源码里面看到是 Call 接口,那么必定是实现类做了操作,我们跟踪 newCall() 方法:

  @Override public Call newCall(Request request) {return RealCall.newRealCall(this, request, false /* for web socket */);}

可以看到里面构建了一个 RealCall ,不管是同步还是异步,其实都是 RealCall 在执行,那我们就看 RealCall 的 同步/异步 实现:

  //同步@Override public Response execute() throws IOException {synchronized (this) {if (executed) throw new IllegalStateException("Already Executed");executed = true;}captureCallStackTrace();eventListener.callStart(this);try {client.dispatcher().executed(this);Response result = getResponseWithInterceptorChain();if (result == null) throw new IOException("Canceled");return result;} catch (IOException e) {eventListener.callFailed(this, e);throw e;} finally {client.dispatcher().finished(this);}}

从源码我们一眼就能看到 RealCall 的同步方法中将执行的逻辑交给了 Dispatcher,其中同步方法交给 Dispatcher 之后马上就执行 getResponseWithInterceptorChain() 方法了,这里涉及了拦截器链的流程,也是真正执行请求的地方,放到另一篇文章分析。Dispatcherexecuted()

  /** Used by {@code Call#execute} to signal it is in-flight. */synchronized void executed(RealCall call) {runningSyncCalls.add(call);}

可以看到,就是将 RealCall 添加到维护的同步队列中。添加进去了,那么什么时候会将这个call移除呢?在 try catch代码块后面有一个finally代码块,这个代码块中执行了 Dispatcherfinished() 方法,注意,这里传的this是 RealCall 对象,我们跳转进去:

  /** Used by {@code Call#execute} to signal completion. */void finished(RealCall call) {finished(runningSyncCalls, call, false);}

这里传进去的队列是同步队列 runningSyncCalls,并且第三个参数是 false :

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {int runningCallsCount;Runnable idleCallback;synchronized (this) {if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");if (promoteCalls) promoteCalls();runningCallsCount = runningCallsCount();idleCallback = this.idleCallback;}if (runningCallsCount == 0 && idleCallback != null) {idleCallback.run();}
}

根据传参我们可以知道里面代码的执行流程是:1、从同步队列中移除call,移除失败时会抛出异常,然后判断 promoteCalls 参数是否为true,这里同步请求为false,不会调用 promoteCalls() 方法,这个方法在异步请求流程中会被调用。2、判断当前正在运行的请求数是否为0,如果为0并且idleCallback不为空时,会执行这个idleCallback。

接着我们再看异步方法的执行流程:

  //异步@Override public void enqueue(Callback responseCallback) {synchronized (this) {if (executed) throw new IllegalStateException("Already Executed");executed = true;}captureCallStackTrace();eventListener.callStart(this);client.dispatcher().enqueue(new AsyncCall(responseCallback));}

异步方法中会创建一个 AsyncCall 传入 Dispatcherenqueue 方法中,我们暂且不看 AsyncCall 类,我们先跟踪 Dispatcherenqueue() 方法:

  synchronized void enqueue(AsyncCall call) {if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {runningAsyncCalls.add(call);executorService().execute(call);} else {readyAsyncCalls.add(call);}}

方法中做了两个判断:1、判断当前正在执行的请求数是否超过最大请求数限制(源码中初始值为64);2、判断当前同一Host的执行请求数是否超过最大限制数(源码中初始值为5)。

如果判断通过则加入正在执行队列,并且交给线程池去执行,线程池也在此时创建:

public synchronized ExecutorService executorService() {if (executorService == null) {executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));}return executorService;
}

判断不通过则加入准备队列,等待执行。

到这里我们就明白了线程池是在什么时机创建的,并且也明白了 Dispatcher 在请求过程中扮演了什么样的角色。
紧接着我们还需要弄明白异步请求过程中两个队列(运行中队列、准备队列)是如何配合工作的,Dispatcher中的 enqueue() 方法会利用线程池去执行异步请求,这时我们就需要跳转到 AsyncCall 类查看:

  final class AsyncCall extends NamedRunnable {private final Callback responseCallback;AsyncCall(Callback responseCallback) {super("OkHttp %s", redactedUrl());this.responseCallback = responseCallback;}//省略部分代码@Override protected void execute() {boolean signalledCallback = false;try {Response response = getResponseWithInterceptorChain();if (retryAndFollowUpInterceptor.isCanceled()) {signalledCallback = true;responseCallback.onFailure(RealCall.this, new IOException("Canceled"));} else {signalledCallback = true;responseCallback.onResponse(RealCall.this, response);}} catch (IOException e) {if (signalledCallback) {// Do not signal the callback twice!Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);} else {eventListener.callFailed(RealCall.this, e);responseCallback.onFailure(RealCall.this, e);}} finally {client.dispatcher().finished(this);}}}

因为 AsyncCall 是一个 NamedRunnable 的实现类,最终会执行到重写的 execute() 方法,方法当中我们又看到了在同步方法中出现过的熟悉的身影 getResponseWithInterceptorChain() ,同样的,我们放到另一个文章中分析。try catch代码块也跟了一个 finally 代码块,这里调用的 finished() 方法中传入的是 AsyncCall 对象,我们跳转进去:

  /** Used by {@code AsyncCall#run} to signal completion. */void finished(AsyncCall call) {finished(runningAsyncCalls, call, true);}

这里调用了3个参数的 finished() 方法,并且传入的第一个参数是运行中的异步队列 runningAsyncCalls ,第三个参数和同步时调用的传参 false 不同,这里为true。我们再看一下重载的3个参数的 finished() 方法, 这里只列出关键的几行代码:

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {//当前分析流程的核心代码synchronized (this) {if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");if (promoteCalls) promoteCalls();}
}

和同步请求的流程一样,会先从运行中的异步队列 runningAsyncCalls 移除call,紧接着因为第三个参数为true,会执行 promoteCalls() 方法:

private void promoteCalls() {//判断目前运行中的异步队列是否超过最大请求数限制if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.//判断准备的异步队列是否为空if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.//遍历准备的异步队列,将其中的请求逐个取出放入运行中队列,并且让线程池执行请求for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {AsyncCall call = i.next();if (runningCallsForHost(call) < maxRequestsPerHost) {i.remove();runningAsyncCalls.add(call);executorService().execute(call);}//每次for循环的最后再判断运行中队列是否超过最大请求数限制,如果超过就结束循环if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.}}

这里就是异步的准备队列、运行中队列通过 Dispatcher 相互配合工作的核心代码,至此 Dispatcher 的调度工作就分析完了。

这篇关于OkHttp源码解析(一)- Dispatcher的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/721911

相关文章

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

在Rust中要用Struct和Enum组织数据的原因解析

《在Rust中要用Struct和Enum组织数据的原因解析》在Rust中,Struct和Enum是组织数据的核心工具,Struct用于将相关字段封装为单一实体,便于管理和扩展,Enum用于明确定义所有... 目录为什么在Rust中要用Struct和Enum组织数据?一、使用struct组织数据:将相关字段绑

使用Java实现一个解析CURL脚本小工具

《使用Java实现一个解析CURL脚本小工具》文章介绍了如何使用Java实现一个解析CURL脚本的工具,该工具可以将CURL脚本中的Header解析为KVMap结构,获取URL路径、请求类型,解析UR... 目录使用示例实现原理具体实现CurlParserUtilCurlEntityICurlHandler

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

数据库使用之union、union all、各种join的用法区别解析

《数据库使用之union、unionall、各种join的用法区别解析》:本文主要介绍SQL中的Union和UnionAll的区别,包括去重与否以及使用时的注意事项,还详细解释了Join关键字,... 目录一、Union 和Union All1、区别:2、注意点:3、具体举例二、Join关键字的区别&php

Spring IOC控制反转的实现解析

《SpringIOC控制反转的实现解析》:本文主要介绍SpringIOC控制反转的实现,IOC是Spring的核心思想之一,它通过将对象的创建、依赖注入和生命周期管理交给容器来实现解耦,使开发者... 目录1. IOC的基本概念1.1 什么是IOC1.2 IOC与DI的关系2. IOC的设计目标3. IOC

java中的HashSet与 == 和 equals的区别示例解析

《java中的HashSet与==和equals的区别示例解析》HashSet是Java中基于哈希表实现的集合类,特点包括:元素唯一、无序和可包含null,本文给大家介绍java中的HashSe... 目录什么是HashSetHashSet 的主要特点是HashSet 的常用方法hasSet存储为啥是无序的

Go中sync.Once源码的深度讲解

《Go中sync.Once源码的深度讲解》sync.Once是Go语言标准库中的一个同步原语,用于确保某个操作只执行一次,本文将从源码出发为大家详细介绍一下sync.Once的具体使用,x希望对大家有... 目录概念简单示例源码解读总结概念sync.Once是Go语言标准库中的一个同步原语,用于确保某个操

Linux中shell解析脚本的通配符、元字符、转义符说明

《Linux中shell解析脚本的通配符、元字符、转义符说明》:本文主要介绍shell通配符、元字符、转义符以及shell解析脚本的过程,通配符用于路径扩展,元字符用于多命令分割,转义符用于将特殊... 目录一、linux shell通配符(wildcard)二、shell元字符(特殊字符 Meta)三、s