OkHttp源码分析(二)Dispatcher分发器 和 AsyncTimeout超时处理

1,492 阅读6分钟

本篇分析两个上一篇提到但没有讲到的两个工具,分别是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。

  1. readyAsyncCalls存储了等待执行的异步请求
  2. runningAsyncCalls存储正在运行的异步请求
  3. 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的职责。

  1. maxRequests表示最大的异步请求数,可以通过setMaxRequests方法进行设置。
  2. 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.Timeoutokio.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的核心--各个拦截器了。