android 开发大多用过Okhttp, 在使用过程中,大多也是同步异步两种方式。一般使用方式如下(基于3.14.2版本):
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
//同步
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
//异步
client.newCall(request).enqueue(new Callback(){
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse( Call call, Response response) throws IOException {
}
});
}
}
由此可见,无论同步异步其实整个请求大致可以分为三个模块, Request,Call,Response。它们就简单概括了一个网络请求的流程:封装请求Request,进行网络请求Call,返回结果Response. 所以我们在使用过程中只需要对这三个模块进行封装。
虽然我们将一个网络请求过程简单的归为三个部分,但是其中的细节才是我们需要清楚的。接下来看client.newCall的内容。
/**
* Prepares the {@code request} to be executed at some point in the future.
*/
@Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
newCall 方法返回了一个RealCall这个类实现了Call的接口, 可以认为这个RealCall是Ok中执行网络请求的一个单位。在RealCall中实现了一个请求的同步异步的两个方法execute()和enqueue().
我们看execute和enqueue的伪代码(暂时不用的地方已经省略)
public Response execute() throws IOException {
try {
//调用okhttpclient中的分发器去执行
client.dispatcher().executed(this);
//将结果直接返回,无论同步异步最终都会调用到getResponseWithInterceptorChain
//这个函数可以看做是整个okhttp的核心
return getResponseWithInterceptorChain();
} finally {
//执行完需要调用分发器中的结束函数
client.dispatcher().finished(this);
}
}
public void enqueue(Callback responseCallback) {
//同样是用dispatcher进行分发,这里是将AsyncCall进行执行
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
AysncCall是RealCall的内部类,这样是为了使用RealCall中的资源,比如okhttpclient,封装的request等等。AsyncCall继承了NameRunnable接口,NameRunable为Runnable的实现,代码如下:
public final void run() {
//更换执行时候的线程名,主要是为了调试的时候能根据name能明白线程的作用
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
//执行此类的抽象方法,NameRunable的子类需要实现这个方法
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
在上面的code中我们发现,无论是同步异步都会通过 client.dispatcher()这个对象去执行,看这个类名我们也可以猜测出这个类应该是负责线程的分发,可以说这个类算是okhttp的调度中心。既然是线程的分发,那么肯定是涉及到了线程池。我们接下来看看Dispather的庐山真面目。
//最大并发请求数,
private int maxRequests = 64;
// 同一个域名最大连接数是5
private int maxRequestsPerHost = 5;
//空闲线程,当dispatcher空闲的时候执行,类似Handler机制中的idleHandler,开发者可视情况选择使用,本文不详述
private @Nullable Runnable idleCallback;
//OkHttp 线程池
private @Nullable ExecutorService executorService;
//准备执行的保存异步Asyncall的有一个等待队列
private final Deque readyAsyncCalls = new ArrayDeque<>();
//保存正在执行AsyncCall的队列
private final Deque runningAsyncCalls = new ArrayDeque<>();
//正在执行的同步RealCall队列
private final Deque runningSyncCalls = new ArrayDeque<>();
Dispatcher就是通过这些内容控制整个过程中线程的并发问题。首先来看线程池的配置
public synchronized ExecutorService executorService() {
if (executorService == null) {
//核心线程为0, 最大线程数为MAX_VALUE理论上可以无限制的开辟线程,
//空闲线程存活60s
//SynchronousQueue为有界队列,大小为0,只是进行生产消费的传递作用
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
其实看完发现和android提供的newCachedThreadPool很类似,只是改了线程名。我们可以简单分析一下这个线程池的运行机制:
如果只是这样就会存在一个很大的问题,如果一直这么创建新的线程, 理论上就可以创建无数线程。 这显然不合理。其实这只是okhttp调度线程的一部分,更重要的部分由readyAsyncCalls runningAsyncCalls runningSyncCalls这部分实现。无论是同步还是异步,httpclient封装一个call传递给调度器。在上面提到的execute或者是enqueue函数我们追查下去都会最终到Dispatcher中的promoteAndExecute()方法中。我们以enqueue为例。
Dispatcher.java
void enqueue(AsyncCall call) {
synchronized (this) {
//将传递进来的call保存进入readyAsyncCalls队列
readyAsyncCalls.add(call);
//找出同域名的请求,如果存在,那么已经存在的call中的callsPerHost数将会被共享,
//callsPerHost数目表示同一个域名下链接的call的数目
//callsPerHost为AtomicInteger保证多线程原子操作性,并且保证线程可见为volatile类型
if (!call.get().forWebSocket) {
AsyncCall existingCall = findExistingCallWithHost(call.host());
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
}
}
promoteAndExecute();
}
private boolean promoteAndExecute() {
//创建一个新的arraylist保存可执行的call
List executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {
//遍历准备执行的队列,如果有数据, 需要将数据视情况去执行并将可以执行的放到正在执行队列
AsyncCall asyncCall = i.next();
//如果目前正在执行的call大于maxRequest默认是64,那么直接停止退出循环
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
//如果这次请求的域名下已经满足最大连接个数(默认是5),那这个call暂时也不能执行
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
//将call中的表示域名连接数的变量自加1.
asyncCall.callsPerHost().incrementAndGet();
//将这个可以执行的call添加到executableCalls链表
executableCalls.add(asyncCall);
//同样添加到正在执行的队列
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
//遍历executableCalls列表,将每个ayncall添加到线程池去执行
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
由上面我们可以看出实际最大执行并发数是64, 并不会真的无限制的增加。最大并发线程数与每个域名同时可以连接数都是可以用户自定义的。
Dispatcher.java
//设置最大并发线程数
public void setMaxRequests(int maxRequests)
//设置相同域名最大并发数
public void setMaxRequestsPerHost(int maxRequestsPerHost)
看到这里我们已经清楚了,call是怎样执行的,同时我们会有疑问,插入和移出必须是成对出现的,线程执行的时候添加到running队列了。那么什么时候移出队列呢?这就要我们继续看AsyncCall的实际执行函数了。
AysncCall的父类NamedRunnable可以当做一个Runnable,从上文我们得知,在它的run方法中都需要执行 execute()方法,而这个方法是抽象方法,需要每个子类去实现。我们看看Ayncall中发生了什么。
RealCall.AsyncCall
protected void execute() {
try {
//获取执行结果
Response response = getResponseWithInterceptorChain();
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 {
//如果发生异常产生错误回调
responseCallback.onFailure(RealCall.this, e);
}
} finally {
//执行完之后,调用dispatcher中的finish函数
client.dispatcher().finished(this);
}
}
}
//我们进一步看看Dispatcher中的finish函数
Dispatcher.java
//这个类重载了多个finished方法,最终调用到这个私有函数里面
private void finished(Deque calls, T call) {
Runnable idleCallback;
synchronized (this) {
//将队列中的元素删除,如果没有这个元素会抛出异常
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}
boolean isRunning = promoteAndExecute();
//如果队列没有在执行的线程且idle线程不为空则执行。 可以看出每个线程结束完都会检查一遍是不是
//要执行空闲线程
if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
以上就是okhttp大致的调度逻辑,本篇文章不涉及各种拦截器的分析。所以我们可以将请求的逻辑分为三个模块