目录
成员变量
1)最大值
2)空闲回调
3)线程池
4)三个队列
同步请求
异步请求
总结
异步请求的执行策略。在OkHttp中承担着对同步和异步请求的分发和回调。今天主要从源码的角度看下这个过程是怎么实现的。
//最多并发请求的个数
private int maxRequests = 64;
//每个主机最大请求数
private int maxRequestsPerHost = 5;
从这里可以看出OkHttp支持最大并发数为64,每个 主机最大的请求数为5。当然也提供了set/get方法去设置或者获取这两个值。
//闲置接口,用来处理当dispatcher变为空闲状态(即所有的请求数为0)时的回调。
private @Nullable Runnable idleCallback;
对应着设置回调的方法为
public synchronized void setIdleCallback(@Nullable Runnable idleCallback) {
this.idleCallback = idleCallback;
}
判断Dispatcher为闲置状态,就是根据runningAsyncCalls.size() + runningSyncCalls.size()之和是否为0,在每次请求结束之后调用finish()时都会做检查来确定是否回调该Runnable。
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;
线程池用来执行每个异步请求。该线程池的加载采用了懒加载,只有在Call去调用到Dispatcher的对应的方法的时候才会去创建该实例。我们也来看看这个线程池的各个参数:
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
核心线程的个数为0,线程池的线程数的最大值为 Integer.MAX_VALUE,超时时间为60s,阻塞队列为同步队列。很显然,这个就是一个每来一个请求都会创建一个新的线程来执行。如果对这个线程池不是很清楚的,可以看下我的其他两篇文章的介绍。
线程池的三种缓存队列
Java提供的四种线程池
//缓存的异步请求
private final Deque readyAsyncCalls = new ArrayDeque<>();
//运行中的异步请求
private final Deque runningAsyncCalls = new ArrayDeque<>();
//运行中的同步请求
private final Deque runningSyncCalls = new ArrayDeque<>();
分别存放着异步或者同步请求。RealCall在执行execute/enqueue时,就是将这次请求加入到Dispatcher的三个集合中。而 Dispatcher通过管理队列集合元素对同步和异步请求进行分发和回调。下面就分析下这个过程。
同步请求call.execute();从源码中可以看到OkHttpClient.newCall返回的就是RealCall。那么也就是调用的RealCall中的execute();
@Override public Response execute() throws IOException {
synchronized (this) {
//......省略代码
try {
client.dispatcher().executed(this);
//......省略代码
return result;
} catch (IOException e) {
//......省略代码
} finally {
client.dispatcher().finished(this);
}
}
进入到Dispatcher中查看execute()里面的代码实现:
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
就是将RealCall加入到runningSyncCalls集合中。这个Dispatcher管理的线程池并没有对同步请求分发进行管理,仅仅用来计算了现在有多少请求。
真正的发送同步请求和处理请求返回的结果在 Response result = getResponseWithInterceptorChain();后面会单独分析。
当处理完返回结果的时候,会调用Dispatcher的finish()来进行将该次的RealCall从队列集合中清空,然后在重新计算Dispatcher是否为空闲状态。
/** Used by {@code Call#execute} to signal completion. */
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}
private void finished(Deque calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
//将该call从集合中清空
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
//如果是true才会调用,也就是异步请求的时候才会调用
if (promoteCalls) promoteCalls();
//计算里面正在运行中请求的数量
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
//如果Dispatcher空闲,则回调idleCallback
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
从RealCall中的代码中可以看到我们在进行同步请求的时候,是在主线程中执行的,所以我们同步请求的代码需要自行放入到子线程中执行。
同样异步请求call.enqueue();也是调用到了RealCall中的enqueue(),进入到源码中可以看到
@Override public void enqueue(Callback responseCallback) {
//.......省略代码
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
进入到Dispatcher的 enqueue()中可以看到在Dispatcher里面会去判断是否到达最大请求数,如果没有的话,直接将AsyncCall加入到runningAsyncCalls集合中,并执行该AsyncCall的run();否则就将AsyncCall加入到缓存队列readyAsyncCalls中。
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
那么我们看下AsyncCall的run()里面做了一些什么呢?NamedRunnable中已经封装了在run()里面执行execute() ,所以直接进入到execute() 查看源码:
@Override protected void execute() {
boolean signalledCallback = false;
try {
//......省略代码
} finally {
client.dispatcher().finished(this);
}
}
我们可以看到同样通过 Response result = getResponseWithInterceptorChain();对发送同步请求和处理请求返回的结果。然后调用Dispatcher的finish()。但是和同步请求不同的是,此时传入的参数发生了变化
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}
也就是除去将队列集合元素清空和重新计算正在运行的请求个数之外,还相应的调用了promoteCalls(),进入到promoteCalls()查看源码:
private void promoteCalls() {
//正在运行的队列中如果超出了最大链接数,直接返回
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
//没有缓存的请求,也直接返回
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
//从请求缓存队列中取出一个元素,加入到运行请求队列中,然后执行该次请求
for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
主要就是从之前的请求缓存队列中取出一个元素,加入到运行请求队列中,然后执行该次请求。
从上面的源码分析下来,我们可以看到异步请求已经在线程池中执行,已经不需要再将异步请求的代码放到子线程中处理了。但是这里我就有一个问题了,为什么不直接使用线程池的缓存策略来处理这个问题,而是需要单独写一套缓存策略呢??希望有人能帮我解答下这样处理的好处。
所以对于Dispatcher对于同步请求和异步请求来说,有以下几点:
1)Dispatcher只是把同步请求放入了Dispatcher中的同步请求的缓存队列中,仅仅用来统计正在运行的请求个数。
2)Dispatcher会对异步请求进行缓存管理,并且有正在运行的异步请求执行完毕,只要没有超出最大链接数,就会自动从缓存中取出一个请求加入到正在运行的请求队列中,等待执行。
3)同步请求在主线程中执行,所以需要将代码放到子线程中去执行;而异步请求已经在子线程中执行了,不在需要开启子线程执行代码。
后面会继续分析OkHttp的源码。