一、介绍
OkHttp 是由 Square 公司贡献的一个处理网络请求的开源项目,是目前 Android 使用最广泛的网络框架,从 Android4.4 开始 HttpURLConnection 的底层实现开始采用 OkHttp,Retrofit 框架底层同样也使用了 OkHttp。官方主页:
OkHttp 有非常多的优点,比如:
- 支持 Http1、Http2、Quic 以及 WebSocket
- 允许对同一主机的所有请求共享一个套接字
- 连接池复用底层 TCP(Socket),减少请求延时
- 无缝支持 GZIP,默认使用 GZIP 压缩数据以减少数据流量
- 响应缓存数据,减少重复的网络请求
- 请求失败自动重试主机的其他 ip,自动重定向
文章中关于 OkHttp 的使用与源码分析会基于 3.10.0 版本,当然我们能查到 3.x 版本在 2020.5.17 之后就不再更新了,而从 4.x 版本开始,OkHttp 改由 Kotlin 实现,后续版本的新东西,有机会会在后面更新。
二、使用方法
还是要简单的提一下使用方法。
2.1 基本使用方法
首先添加 OkHttp 依赖:
implementation "com.squareup.okhttp3:okhttp:3.10.0"
OkHttp 支持同步或异步发起一个网络请求,我们先用同步方式执行一个 GET 请求:
// 同步方式不能在主线程中执行
private void executeGet() throws IOException {
// 获取默认配置的 OkHttpClient
OkHttpClient client = new OkHttpClient();
// 构造一个 GET 请求
Request request = new Request.Builder()
.url("https://www.baidu.com")
.build();
// 获取网络请求的执行任务
Call call = client.newCall(request);
// 同步方式执行任务
Response response = call.execute();
// 打印响应体
Log.d(TAG, "response body:" + response.body().string());
}
用异步方式执行一个 POST 请求:
// 异步方式可以在主线程中执行
public void executePost() {
// 自定义一个 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.writeTimeout(2000, TimeUnit.MICROSECONDS)
.build();
// 构造一个 POST 请求
RequestBody requestBody = new FormBody.Builder()
.add("param1", "111")
.build();
Request request = new Request.Builder()
.url("https://www.baidu.com")
.post(requestBody)
.build();
// 异步方式执行请求
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.d(TAG, "Post request failed.");
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d(TAG, "Post request succeed,response body:" + response.body().string());
}
});
}
2.2 调用流程
从上述例子中可以看出,使用 OkHttp 进行网络请求至少需要接触到 OkHttpClient、Request、Call 和 Response,框架内还会进行大量的逻辑处理,流程大致如下:
- 首先创建一个 OkHttpClient 对象,可以通过构造方法获得一个默认配置的 OkHttpClient,也可以通过 OkHttpClient.Builder 根据需求自己配置一个
- 根据需求创建一个网络请求对象 Request,并作为参数传递给 OkHttpClient 的 newCall() 生成一个任务 Call(newCall() 其实会调用实现类 RealCall 的 newRealCall() 返回一个 RealCall 对象)
- 如需同步执行任务则调用 Call 的 execute(),异步执行任务调用 Call 的 enqueue()
- 任务不论是同步还是异步执行,都会交由分发器 Dispatcher 负责存入相应的队列,并调度任务去执行
- 执行任务的核心是拦截器 Interceptors,OkHttp 内置的五大拦截器会帮助我们将网络请求发出,并接收服务器的响应封装到 Response 后返回
其实分发器和拦截器为我们做了大量的复杂且细致的工作,只不过由于 OkHttp 采用了门面模式,将这些细节都隐藏起来,仅通过 OkHttpClient 对象统一暴露子系统接口。
三、分发器 Dispatcher
Dispatcher 主要用来调配请求任务,其内部包含一个线程池,在用构建者模式创建 OkHttpClient 对象时,可以通过 dispatcher() 指定一个自定义的分发器(指定线程池、闲时任务等)。分发器内的主要成员有:
public final class Dispatcher {
// 异步请求同时存在的最大请求
private int maxRequests = 64;
// 异步请求同一域名同时存在的最大请求
private int maxRequestsPerHost = 5;
// 闲时任务(没有请求时,可以去执行的其它任务)
private @Nullable Runnable idleCallback;
// 异步请求使用的线程池,懒创建线程池对象
private @Nullable ExecutorService executorService;
// 异步请求等待执行的任务队列
private final Deque<RealCall.AsyncCall> readyAsyncCalls = new ArrayDeque<>();
// 异步请求正在执行中的任务队列
private final Deque<RealCall.AsyncCall> runningAsyncCalls = new ArrayDeque<>();
// 同步请求正在执行中的任务队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
}
我们注意到异步任务类型是 RealCall.AsyncCall,并且分成了执行队列和等待队列;而同步任务类型是 RealCall,仅有执行队列。
下面我们就来分析同步请求和异步请求的任务是如何被调度的。
3.1 同步请求
同步请求的流程相较于异步请求要简单不少,所以我们先来看同步请求。
调用 Call 的 execute() 会以同步方式执行该任务,要看实现类 RealCall 的具体处理:
@Override
public Response execute() throws IOException {
// 防止被重复调用
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
// 任务入队要交给 Dispatcher
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 {
// 无论是成功得到了结果还是抛出了异常,都要让 Dispatcher 标记当前任务已经结束
client.dispatcher().finished(this);
}
}
Dispatcher 会将任务添加到同步队列中:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
在任务执行结束后,Dispatcher 会将任务从队列中移除:
void finished(RealCall call) {
// 将 call 从 runningSyncCalls 队列中移除,false 表示不需要优化任务调度,
// 因为只有在进行异步任务调度时,才会用到,后面介绍异步任务时会看到
finished(runningSyncCalls, call, false);
}
3.2 异步请求
先来看一下异步任务的执行过程:
- AsyncCall 调用 enqueue() 以异步方式执行,Dispatcher 负责将 AsyncCall 入队,入队前,先看当前队列状态是否可以将 AsyncCall 加入运行队列 runningAsyncCalls,条件满足则放入,并且放入线程池 ThreadPool 中执行,否则放入等待队列 readyAsyncCalls 中;
- 在 ThreadPool 中的任务被执行后,会调用 AsyncCall 的 finished() 来结束该任务,finished() 主要做了两件事:
2.1. 将该任务从 runningAsyncCalls 中移除
2.2.调用 promoteCalls(),去 readyAsyncCalls 中筛选是否有任务满足条件可以添加到 runningAsyncCalls 中,如有,则将其添加到 runningAsyncCalls 内,且放入线程池中
以上是异步任务的执行流程,至于图中的两个进入 runningAsyncCalls 的条件,其实是 runningAsyncCalls 中的任务数没有达到 maxRequests,且对于同一 Host 的请求数没有到达 maxRequestsPerHost,后续我们结合源码看会更清晰一些。
结合上图来看源码,首先,调用 Call.enqueue() 异步执行任务时,实际上调用的是 RealCall 的 enqueue():
@Override
public void enqueue(Callback responseCallback) {
// 第二次调用 enqueue() 就会抛异常,保证最多被执行一次
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
// 通过 Dispatcher 的 enqueue() 将任务入队,而且任务类型是 RealCall.AsyncCall
client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
Dispatcher 在拿到 AsyncCall 这个任务后,会根据当前异步任务队列的状态决定把它放到哪个队列:
synchronized void enqueue(RealCall.AsyncCall call) {
// 正在运行的异步任务数小于最大值(默认64),并且同一域名的异步请求小于最大值(默认5)
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
// 放入正在运行的队列,并放入线程池中
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
// 放入等待队列中
readyAsyncCalls.add(call);
}
}
假如命中了 if 条件,将 AsyncCall 放入正在执行队列,并放入线程池中执行该任务,那么 AsyncCall 作为 Runnable 的一个实现类,它的 run() 包含什么样的任务呢?追溯源码,我们发现 AsyncCall 并没有重写 run(),这是由于它继承自一个抽象类 NamedRunnable:
public abstract class NamedRunnable implements Runnable {
protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}
@Override
public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
NamedRunnable 希望子类将需要执行的任务写在 execute() 中,所以我们回头看 AsyncCall 的 execute():
@Override
protected void execute() {
// signalledCallback 表示用户是否赋值了 Callback
boolean signalledCallback = false;
try {
// 通过责任链发送请求并得到响应,责任链后面会详解
Response response = getResponseWithInterceptorChain();
// 当 try 代码块中运行出现异常进入 catch 时,如果 signalledCallback 为 true
// 表示是用户的 Callback 中发生了异常,否则就是上面的由 OkHttp 的责任链
// 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 {
// 无论是成功执行还是发生了异常,都会让 Dispatcher 结束这个任务
client.dispatcher().finished(this);
}
}
不管任务执行成功与否,最后都需要让 Dispatcher 结束这个任务:
// 异步任务结束
void finished(RealCall.AsyncCall call) {
finished(runningAsyncCalls, call, true);
}
// 同步任务结束
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
// 将任务 call 从队列 calls 中移除
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
// 异步任务的 promoteCalls 为 true,同步为 false,promoteCalls() 会将处于等待
// 状态、并且符合条件的异步任务,从其所在队列中拿到正在运行的异步任务队列当中去
if (promoteCalls) promoteCalls();
// 计算正在运行的任务数量,这是运行中的同步和异步任务数量之和
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
// 如果当前没有正在运行的任务,且设置了闲时任务,则开始执行闲时任务
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
promoteCalls() 并不是简单的从 readyAsyncCalls 队列中将队首任务取出放入 runningAsyncCalls 队列,而是有条件的筛选:
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<RealCall.AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
RealCall.AsyncCall call = i.next();
// 保证同一个 Host 的任务数不超过 5 个的情况下,才会移动 call
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
以上就是异步请求的大致流程。
3.3 线程池
Dispatcher 调度异步任务离不开线程池。这个线程池对象可以通过 Dispatcher 的构造方法指定,如果没有显式指定的话,Dispatcher 内部会创建一个默认的线程池对象:
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;
}
ThreadPoolExecutor() 各参数含义如下:
- 核心线程数:核心线程池的大小。如果核心线程池有空闲位置,新来的任务就会被核心线程池新创建的线程执行。默认情况下,核心线程在执行完任务,处于空闲状态时也不会被回收,而是在缓存队列中等待再次被运行,除非你手动将 ThreadPoolExecutor 的 allowCoreThreadTimeOut 设置为 true(默认为 false),那么核心线程将会与非核心线程一样,在空闲时间到达第三、四个参数组成的保活时间之后,被销毁。
- 最大线程数:线程池能创建最大的线程数量,是核心线程数与非核心线程数的和。如果核心线程池和缓存队列都已经满了,新任务进来就会创建新的线程来执行,但是数量不能超过最大线程数,否则会采取拒绝接受任务策略(拒绝策略可以在 ThreadPoolExecutor 的构造方法中配置,但是当前方法并没有配)。
- 保活时间:默认情况下指非核心线程处于空闲等待状态的最长时间,一旦超过这个时间就会被销毁。核心线程在设置 allowCoreThreadTimeOut 为 true 之后也会受到这个时间的约束。
- 保活时间单位:保活时间的单位,有天、小时、分钟、秒等。
- 线程等待队列:缓存队列,用来存放等待执行中的任务,需是一个阻塞队列,一般可以是 ArrayBlockingQueue、LinkedBlockingQueue 和 SynchronousQueue 三者之一。
- 线程创建工厂:创建线程的工厂,这里是给创建的线程命名为"OkHttp Dispatcher",且设置不是守护线程。
了解了以上参数后,再来看看线程池是如何调度任务的。
线程池调度机制
当我们通过 ThreadPoolExecutor 的 execute() 提交一个任务进线程池时,线程池是如何处理这个任务的?来看下图:
基本思想还是本着【核心线程->阻塞队列->空闲线程/新建线程/拒绝】的优先顺序来执行任务,只有上一级条件不满足时才会由下一级执行。
具体的处理顺序:
- 如果当前线程池中的线程数量小于最大核心线程数,那么创建一个新的核心线程来处理该任务
- 如果当前线程数量不小于最大核心线程数,那就要尝试将新任务添加到阻塞队列中
- 添加队列成功,如果当前线程池中没有任何线程,那就新建一个非核心线程来执行任务;如果线程池有线程,那就让任务等待空闲线程执行自己
- 添加队列失败,如果线程数量小于最大线程数,则创建一个新线程执行任务,否则就只能根据拒绝策略拒绝该任务了
简单看下 execute() 的源码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 1.如果运行中的线程数量小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 创建一个新的核心线程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果核心线程满载了,并且任务被成功添加到队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// double-check 一下是否应该新建一个线程执行这个任务,因为可能会有一个
// 自上次检查之线程池的状态发生了改变,比如变为 SHUTDOWN 状态,这样的话
// 还需要回滚入队操作,并拒绝这个任务
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果线程池中一个线程都没有,那就新起一个线程执行任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3.任务不能入队,先尝试启动一个非核心新线程,如果失败,则只能拒绝该任务
else if (!addWorker(command, false))
reject(command);
}
实际上,线程池调度的细节内容还是很多的,篇幅受限我们仅了解整体的调度思路,帮助我们更好的理解 OkHttp 对线程池的应用。
OkHttp 配置线程池
我们再回头看一下 OkHttp 对其内部默认线程池的配置:
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;
}
这个线程池的配置,其实和 Executors.newCachedThreadPool() 创建的缓存线程池一样(但是之前看阿里的编程规范中规定,强制不允许使用 Executors 创建线程池,原因是使用 new ThreadPoolExecutor() 这种方式能让使用者更加明确线程池的运行规则,规避资源耗尽的风险),核心线程数为 0,表示线程池不会一直为我们缓存线程,所有线程在空闲时间到达 60s 后就会被回收,并且有无等待,高并发,最大吞吐量的特点。
前面我们也提到,异步队列有三种:SynchronousQueue、ArrayBlockingQueue 和 LinkedBlockingQueue,那 OkHttp 为什么选择了 SynchronousQueue?这其实与线程池的任务调度机制有关。
记得,OkHttp 配置的默认线程池的核心线程数为 0,这是前提。
先来看 ArrayBlockingQueue,该队列基于数组,初始化时必须显式指定一个队列容量,在队列满了之后,可能会出现后提交的任务先被执行,而先提交的任务一直处于等待状态的问题。示例代码:
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), threadFactory(false));
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务1:" + Thread.currentThread());
while (true) {
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务2:" + Thread.currentThread());
}
});
/*executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("任务3:" + Thread.currentThread());
}
});*/
}
我们设置了 ArrayBlockingQueue 的容量为 1,先执行任务 1,内部的 while 死循环模拟执行时间较长、队列已满的情况,然后执行任务 2,猜一下此时的运行结果是什么呢?
任务1:Thread[Thread-0,5,main]
任务 2 并没有得到执行!而在打开任务 3 的注释,去执行任务 3,神奇的事情发生了:
任务1:Thread[Thread-0,5,main]
任务3:Thread[Thread-1,5,main]
任务2:Thread[Thread-1,5,main]
任务 2 和任务 3 都被同一个线程执行了!并且任务 3 先于任务 2 执行!发生这种现象的原因,要结合线程池调度机制来看。我们叙述一下示例代码的执行步骤:
- 初始条件:线程池没有任何线程,ArrayBlockingQueue 容量为 1
- 执行任务 1,由于现在没有线程,那么任务 1 要进入 ArrayBlockingQueue。入队成功后,会做个检查,如果当前一个线程都没有,那就新起一个线程执行任务 1,执行它时会将其从 ArrayBlockingQueue 中移除
- 再执行任务 2,由于此时没有核心线程和空闲线程,但是 ArrayBlockingQueue 中有位置,所以任务 2 可以入队,入队成功的它会等待一个空闲线程来执行自己。但是当前线程池中唯一的线程在执行任务 1,而任务 1 执行时间很长,导致一直没有空闲线程,所以任务 2 就一直等待,得不到执行
- 最后执行任务 3,由于此时还是没有核心线程和空闲线程,而且 ArrayBlockingQueue 也满了(容量为 1,被任务 2 占据),所以任务 3 入队失败。由于当前线程池中的线程数小于最大线程数,所以可以新创建一个线程执行任务 3
- 任务 3 被执行完毕后,执行它的线程空闲了,等待了许久的任务 2 终于等来了一个空闲线程来执行自己,所以任务 2 最后被执行
LinkedBlockingQueue 与 ArrayBlockingQueue 存在同样的问题,即便不显式指定容量,其内部也会使用默认的 Integer.MAX 作为队列容量。
SynchronousQueue 是没有容量的队列,添加任务会一直失败,在没有空闲线程且线程数量没达到最大线程数的情况下,会直接起一个新线程执行任务,这是使用 SynchronousQueue 的主要原因。这种线程池的配置也满足高并发、最大吞吐量的特点,线程池的作用也集中体现在线程复用上。当然,即便使用了 SynchronousQueue 线程数也不会达到线程池定义时设置的 Integer.MAX 那么多,因为 OkHttp 不是定义了最大请求数 maxRequests = 64 限制着呢么~