点赞关注,不再迷路,你的支持对我意义重大!
Hi,我是丑丑。本文「Android 路线」| 导读 —— 从零到无穷大 已收录。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
前言
- 网络请求是 App 中非常重要的一个组件,而 OkHttp 作为官方和业界双重认可的解决方案,其学习价值不必多言;
- 在这篇文章里,我将分析 OkHttp 分发器 & 拦截器 的实现原理。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
目录
1. 前置知识
Editting...
2. OkHttp 简介
OkHttp 是 Square 开源的网络请求框架,自从 Android 4.4 移除 HttpURLConnection 后,逐渐演变成 Android 端最主流的网络请求框架。
- OkHttp Github 仓库
- OkHttp 官网
2.1 优点
- 1、支持 Http1、Http2、Quic 以及 WebSocket 等多种应用层协议;
- 2、连接池复用底层 TCP 连接(Socket),降低了请求时延;
- 3、无缝支持 GZIP 减少数据流量;
- 4、缓存响应数据,减少了重复网络请求;
- 5、自动失败重试、自动重定向。
2.2 使用流程
一次 OkHttp 请求过程最少需要用到 OkHttpClient、Request、Call、Response,例如:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
同步请求
Call call = client.newCall(request);
获得响应
Response response = call.execute();
异步请求
Call call = client.newCall(request);
call.enqueue(new Callback(){
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) {
获得响应
}
});
需要注意的是,Call 是一个接口,OkHttpClient#newCall(...)
返回的其实是它的实现类 RealCall:
OkHttpClient.java
public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false);
}
RealCall.java
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
3. Dispatcher 分发器
Call#execute() & Call#enqueue(...)
分别表示调用同步请求和异步请求,客户端对请求任务的调度过程是不感知的。这是因为 OkHttp 内部的 「Dispatcher 分发器」 封装了整个请求任务的调度过程,这一节我们来具体分析下。
3.1 自定义 Dispatcher 分发器
Dispatcher 主要成员变量如下:
Dispatcher.java
异步请求最大并发数
private int maxRequests = 64;
同一域名请求最大并发数
private int maxRequestsPerHost = 5;
闲置任务(没有请求时执行)
private Runnable idleCallback;
线程池
private ExecutorService executorService;
异步请求等待队列
private final Deque readyAsyncCalls = new ArrayDeque();
异步请求执行队列
private final Deque runningAsyncCalls = new ArrayDeque();
同步请求执行队列
private final Deque runningSyncCalls = new ArrayDeque();
其中,以下几个变量是允许自定义的:
变量 | 描述 | 默认值 |
---|---|---|
maxRequests | 异步请求最大并发数 | 64 |
maxRequestsPerHost | 同一域名请求最大并发数 | 5 |
idleCallback | 闲置任务 | null |
executorService | 线程池 | 无等待,最大并发 |
提示: 限制请求数是为了避免客户端和服务器的负载过大(Linux 一切皆文件,Socket 占用文件文件句柄,打开文件数是有限制的),至于为什么默认值为 64 和 5,据说 OkHttp 是参考了主流浏览器得出的经验值,未查及论据。
自定义的 Dispatcher 对象通过 OkHttpClient.Builder 构建者设置:
OkHttpClient.java
public OkHttpClient.Builder dispatcher(Dispatcher dispatcher) {
if (dispatcher == null) {
throw new IllegalArgumentException("dispatcher == null");
} else {
this.dispatcher = dispatcher;
return this;
}
}
3.2 同步请求
这一节我们来分析 OkHttp 同步请求的执行过程:
RealCall.java
已简化
public Response execute() throws IOException {
1、禁止重复执行
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
2、记录
this.client.dispatcher().executed(this);
3、执行请求,阻塞等待响应返回
Response result = this.getResponseWithInterceptorChain();
4、移除记录
this.client.dispatcher().finished(this);
return result;
}
Dispatcher.java
记录到 runningSyncCalls 中
synchronized void executed(RealCall call) {
this.runningSyncCalls.add(call);
}
从 runningSyncCalls 中移除
void finished(RealCall call) {
内部逻辑见 第 3.3 节
finished(runningSyncCalls, call);
}
同步请求是在当前线程阻塞执行,所以不需要 Dispatcher 调度任务,Dispatcher 内部也仅仅是通过 runningSyncCalls 记录同步请求。
同时可以看到,RealCall#getResponseWithInterceptorChain()
是真正执行请求的地方,我一并在 「Android 路线」| OkHttp 拦截器 中分析。
3.3 异步请求
这一节我们来分析 OkHttp 异步请求的执行过程:
RealCall.java
已简化
public void enqueue(Callback responseCallback) {
1、禁止重复执行
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}
this.executed = true;
}
2、调用分发器
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}
OkHttp 异步请求就需要 Dispatcher 登场了,我们来看看:
Dispatcher.java
void enqueue(AsyncCall call) {
synchronized (this) {
2.1 加入 readyAsyncCalls
readyAsyncCalls.add(call);
if (!call.get().forWebSocket) {
2.2 复用同一个计数器(记录同一域名异步请求数)
AsyncCall existingCall = findExistingCallWithHost(call.host());
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
}
}
2.3 触发或执行请求
promoteAndExecute();
}
主要的逻辑应该在 Dispatcher#promoteAndExecute()
中:
2.3 触发或执行请求
private boolean promoteAndExecute() {
List executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
1、遍历 readyAsyncCalls 列表
for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
2、判断全部异步请求并发数是否超过限制
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
3、判断同一域名异步请求并发数是否超过限制
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
4、符合执行条件
i.remove();
4.1、同一域名异步请求数加一
asyncCall.callsPerHost().incrementAndGet();
4.2 可执行任务列表
executableCalls.add(asyncCall);
4.3 执行中任务列表
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
5 处理可执行任务列表
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
执行请求
asyncCall.executeOn(executorService());
}
6 返回是否正在执行请求(若无请求,则执行闲置任务 idleCallback)
return isRunning;
}
可以看到,异步请求是存在限制的,只有 「全部异步请求并发数不超过限制」 & 「同一域名异步请求并发数不超过限制」,才允许执行,否则会停留在 readyAsyncCalls 中等待。
那么,谁来拯救在 readyAsyncCalls 中等待的请求呢?其实就是看什么时候会触发promoteAndExecute()
方法了,除了 setMaxRequests(int)、setMaxRequestsPerHost(int)
外,在异步请求完成后,让出 “空闲名额” 也会触发。
AsyncCall.java
5 处理可执行任务列表
void executeOn(ExecutorService executorService) {
boolean success = false;
try {
5.1 线程池执行
executorService.execute(this);
success = true;
} catch (RejectedExecutionException e) {
5.2 线程池拒绝执行
InterruptedIOException ioException = new InterruptedIOException("executor rejected");
ioException.initCause(e);
eventListener.callFailed(RealCall.this, ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
5.3 finish
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}
5.1 线程池执行(已简化)
protected void execute() {
try {
5.1.1 执行请求,阻塞等待响应返回
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
5.1.2 请求取消
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
5.1.3 请求成功
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
5.1.4 请求失败
responseCallback.onFailure(RealCall.this, e);
} finally {
5.1.5 finish
client.dispatcher().finished(this);
}
}
这段代码不算复杂,AsyncCall 是 Runnable 的子类,Runnable#run()
最终会走到AsyncCall#execute()
,主要分为三步:
- 调用
getResponseWithInterceptorChain()
来获得请求响应,见 「Android 路线」| OkHttp 拦截器; - 调用 responseCallback 回调;
- 调用 Dispatcher#finished()。
Dispatcher#finished() 我们在 第 3.2 节 同步请求里见过,现在我们来分析下:
Dispatcher.java
5.1.5 finish 或 5.3 finish
private void finished(Deque calls, T call) {
Runnable idleCallback;
synchronized (this) {
1、从移除请求执行列表中移除
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}
2、触发或执行请求
boolean isRunning = promoteAndExecute();
3、若无请求,则执行闲置任务 idleCallback
if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
4. 分发器线程池
为什么不能使用ArrayBlockingQueue()
为什么不能使用LinkedBlockingQueue
为什么使用SynchronousQueue
5. 总结
看到这里,我们先来总结这篇文章的内容以及遇到的疑问:
- 1、同步请求不需要 Dispatcher 任务调度,Dispatcher 只做记录;
- 2 、异步请求有限制,被限制的请求会在 ready 队列中等待,直到有请求完成让出 “空闲名额” 才会触发执行;
在前面关于同步请求和异步请求的分析中,我们都提到了 RealCall#getResponseWithInterceptorChain()
是真正执行请求的地方,我在 「Android 路线」| OkHttp 拦截器 里讨论。
创作不易,你的「三连」是丑丑最大的动力,我们下次见!