本篇分析两个上一篇提到但没有讲到的两个工具,分别是Dispatcher 和 AsyncTimeout
Dispatcher分发器
这个类负责异步请求的调度,内部使用了线程池来进行异步调度。并负责收集正在进行的同步请求,就像一个调度室,你可以获取当前正在执行的同步/异步请求和正在等待的异步请求。甚至可以调用cancelAll()取消所有请求。
外部使用Dispatcher的方法很简单,上一篇也介绍过。
executed(RealCall call)同步请求时调用finished(RealCall call)同步请求结束时调用enqueue(AsyncCall call)异步请求时调用finished(AsyncCall call)异步请求结束时调用cancelAll()取消所有请求。 可以看出不同和异步请求使用了不同的数据类型,同步直接使用了RealCall,AsyncCall则是对RealCall的封装,因为要使用线程池调度,所以继承了Runnable。
我们分功能点细致分析下每个功能点。
存储数据结构
/** 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<>();
内部的存储结构使用了Deque双端队列。可以同时对两端进行进出操作。
这里有三个队列存储数据。分别是readyAsyncCalls/runningAsyncCalls/runningSyncCalls。
- readyAsyncCalls存储了等待执行的异步请求
- runningAsyncCalls存储正在运行的异步请求
- runningSyncCalls请求正在执行的同步请求
异步请求
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}
异步请求调用了enqueue方法进行请求,先直接放到了等待的异步请求队列中。再通过promoteAndExecute方法直接请求统一调度。
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}
return isRunning;
}
内部的逻辑先遍历readyAsyncCalls,这里存储刚刚调用enqueue的传入的AsyncCall。有两个闲置条件,如果超过了最大请求数,那么就不能继续执行。isRunning变量表示当前是否有正在运行的同步异步请求。 如果调用enqueue进行异步请求,但是已经超过最大容量了,有什么时机保证会执行到呢。
private <T> void finished(Deque<T> calls, T call) {
。。。
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
。。。
}
boolean isRunning = promoteAndExecute();
。。。
}
答案就在finished方法中,这个方法在同步和异步请求执行完成后,都会调用。先进性remove,再调用promoteAndExecute方法进行调度处理。如果现在满足最大容量要求了,就会继续运行。
这样就保证了异步请求肯定得到运行。
同步请求
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
同步请求直接存储了这个call在runningSyncCalls中。为什么只做了存储操作呢,因为同步请求外部已经在外部直接请求了拦截器链进行处理了。等同步请求完成后,调用finished进行回收。
void finished(RealCall call) {
finished(runningSyncCalls, call);
}
对于同步请求的保存,有什么用处呢?
- 首先保存了正在进行的同步请求,外部可以获取这个信息。
- 其次外部可以调用
cancelAll()取消所有请求。这里满足了取消的需求。
请求数量限制
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
对于OKHttp的最大请求数,这个也是Dispatcher的职责。
- maxRequests表示最大的异步请求数,可以通过
setMaxRequests方法进行设置。 - maxRequestsPerHost,表示每个host能异步请求的最大数量。这里host在OKHttp的定义是主机地址,下面给出了一些例子,并不是IP地址。
| 网址 | host |
|---|---|
| android.com/ | android.com |
| http://127.0.0.1/ | 127.0.0.1 |
| xn--n3h.net/ | xn--n3h.net |
闲置回调
private @Nullable Runnable idleCallback;
OKHttp内部提供了闲置回调,也就是说当前同步和异步请求都没有运行时,会回调idleCallback。
可以通过setIdleCallback(@Nullable Runnable idleCallback)设置闲置回调。
具体执行逻辑的代码如下:
private <T> void finished(Deque<T> 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();
if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
在网络请求完成后,高层需要调用finished方法表示请求完成,Dispatcher会移除相应地监听。promoteAndExecute方法内部会通过runningCallsCount()方法获取运行的请求数量。也就是上面说的当前同步和异步请求都没有运行时,会调用idleCallback.run();
public synchronized int runningCallsCount() {
return runningAsyncCalls.size() + runningSyncCalls.size();
}
线程池
private @Nullable ExecutorService executorService;
Dispatcher内部会自己懒加载一个线程池,我们也可以通过构造方法,自己定制一个线程池。但是这个线程池需要满足最大容量的要求,比如最大同时容量是10,我们的线程池不能总共只有不到10个可用。
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内部创建的线程池,没有核心线程,最大工作线程为Integer.MAX_VALUE,并闲置60秒后会被回收。
取消请求操作
Dispatcher提供了取消全部请求的API
public synchronized void cancelAll() {
for (AsyncCall call : readyAsyncCalls) {
call.get().cancel();
}
for (AsyncCall call : runningAsyncCalls) {
call.get().cancel();
}
for (RealCall call : runningSyncCalls) {
call.cancel();
}
}
直接调用了所有存储的丢嘞,对RealCall调用了cancel方法。逻辑比较简单。
@Override public void cancel() {
retryAndFollowUpInterceptor.cancel();
}
public void cancel() {
canceled = true;
StreamAllocation streamAllocation = this.streamAllocation;
if (streamAllocation != null) streamAllocation.cancel();
}
底层的请求调用了StreamAllocation的cancel方法。又出现了一个新的类,这个类很重要,我们讲RetryAndFollowUpInterceptor会讲。
AsyncTimeout超时处理
AsyncTimeout类主要负责请求超时的工作。在上一篇介绍OkHttpClient中,我们介绍了超时的几个字段,分别是:
final int callTimeout; //整体请求的时间限制
final int connectTimeout; // socket connent 连接的时间限制
final int readTimeout; // socket读取数据的时间限制
final int writeTimeout; // socket写入时间限制
下面会通过对AsyncTimeout的分析,介绍超时字段的工作原理。
AsyncTimeout继承自okio.Timeout,okio.Timeout类主要负责设置超时时间,设置的方式有两种
- 通过
timeout()方法设置运行的时间,运行了制定时间就会触发超时 - 通过
deadlineNanoTime()设置终点deadline时刻,到达这个终点时间就会超时 设置完成时间数据后,使用AsyncTimeout的相关方法进行超时处理。我们就可以AsyncTimeout#enter()开始计时,使用AsyncTimeout#exit()表示执行完成,计时也会停止。如果在执行完成前超过了设置的时间限制,触发了超时,就会调用AsyncTimeout#timeOut(),这是一个protected方法,需要重写定制自己的超时逻辑。
整体的超时处理流程就是这样,看下如何实现超时的。调用enter方法后,内部会调用scheduleTimeout开启超时的线程,
static @Nullable AsyncTimeout head;
/** True if this node is currently in the queue. */
private boolean inQueue;
/** The next node in the linked list. */
private @Nullable AsyncTimeout next;
private static synchronized void scheduleTimeout(
AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
if (head == null) {
head = new AsyncTimeout();
new Watchdog().start();
}
long now = System.nanoTime();
if (timeoutNanos != 0 && hasDeadline) {
node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
} else if (timeoutNanos != 0) {
node.timeoutAt = now + timeoutNanos;
} else if (hasDeadline) {
node.timeoutAt = node.deadlineNanoTime();
} else {
throw new AssertionError();
}
// Insert the node in sorted order.
long remainingNanos = node.remainingNanos(now);
for (AsyncTimeout prev = head; true; prev = prev.next) {
if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
node.next = prev.next;
prev.next = node;
if (prev == head) {
AsyncTimeout.class.notify();
}
break;
}
}
}
内部使用一个链表进行处理,head表示链表头部、next表示链表的下一个链节、inQueue表示当前是否在链表中。调用scheduleTimeout,如果当前没有头结点,那么就会创建一个空的头节点。并启动Watchdog这个线程,开始计时。并在下面的逻辑中,把当前节点插入到链表中,根据当前的时间从先到后的顺序。 Watchdog是一个Thread,具体的执行逻辑在run中。
public void run() {
while (true) {
try {
AsyncTimeout timedOut;
synchronized (AsyncTimeout.class) {
timedOut = awaitTimeout();
if (timedOut == null) continue;
if (timedOut == head) {
head = null;
return;
}
}
// Close the timed out node.
timedOut.timedOut();
} catch (InterruptedException ignored) {
}
}
}
awaitTimeout方法拿到当前的已经超时的AsyncTimeout,并直接执行他的timeOut方法,表示已经超时。如果返回的已经是头节点了,那么表示整个队列已经完成超时了,并退出这个while循环。
private static final long IDLE_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(60);
static @Nullable AsyncTimeout awaitTimeout() throws InterruptedException {
// Get the next eligible node.
AsyncTimeout node = head.next;
if (node == null) {
long startNanos = System.nanoTime();
AsyncTimeout.class.wait(IDLE_TIMEOUT_MILLIS);
return head.next == null && (System.nanoTime() - startNanos) >= IDLE_TIMEOUT_NANOS
? head // The idle timeout elapsed.
: null; // The situation has changed.
}
long waitNanos = node.remainingNanos(System.nanoTime());
// The head of the queue hasn't timed out yet. Await that.
if (waitNanos > 0) {
// Waiting is made complicated by the fact that we work in nanoseconds,
// but the API wants (millis, nanos) in two arguments.
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
return null;
}
// The head of the queue has timed out. Remove it.
head.next = node.next;
node.next = null;
return node;
}
内部主要是通过AsyncTimeout.class.wait(waitMillis, (int) waitNanos)进行等待处理的。如果这个链表只剩头节点head,也会等待IDLE_TIMEOUT_MILLIS值的时间,过了这段时间,这个线程就会退出,这段时间内还是可以继续往里面添加元素的。
因为这个链表是通过时间从先到后排序的,所以这里按照链表进行await依次等待。逻辑比较简单。
完成操作的exit()怎么实现的。
/** Returns true if the timeout occurred. */
public final boolean exit() {
if (!inQueue) return false;
inQueue = false;
return cancelScheduledTimeout(this);
}
private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
for (AsyncTimeout prev = head; prev != null; prev = prev.next) {
if (prev.next == node) {
prev.next = node.next;
node.next = null;
return false;
}
}
return true;
}
exit()方法内部直接清空了这个链表。这样就不会继续触发超时了。 AsyncTimeout的逻辑比较简单,整体使用链表处理超时,超时的处理是通过await方法实现的。创建的多个AsyncTimeout,并调用enter,都会放到静态的head后面,等待处理。超时了就调用timeOut方法表示超时。设计比较巧妙。
下面的文章就要分析OKHttp的核心--各个拦截器了。