Android OkHttp3源码详解——Dispatcher(任务调度器)

一 前言

前一篇文章我们学习了Android OkHttp3源码详解——整体框架,今天我们来看一下Dispatcher(调度器)。首先我们来看看OkHttp的Wiki对Dispatcher`(调度器)介绍。

For synchronous calls, you bring your own thread and are responsible for managing how many simultaneous requests you make. Too many simultaneous connections wastes resources; too few harms latency.

对于同步调用,你用自己的线程负责管理有多少并发请求。太多的并发连接浪费资源;特别是导致延迟。

For asynchronous calls, Dispatcher implements policy for maximum simultaneous requests. You can set maximums per-webserver (default is 5), and overall (default is 64).

对于异步调用,Dispatcher实现最多的并发请求策略。你可以设置每个主机最大请求数(默认为5),和最大并发请求数(默认是64)。

下面就来看Dispatcher(调度器)的源码。

二 Dispatcher(调度器)源码解析

public final class Dispatcher {
  /** 最大并发请求数为64 */
  private int maxRequests = 64;
  /** 每个主机最大请求数为5 */
  private int maxRequestsPerHost = 5;

  /** 线程池 */
  private ExecutorService executorService;

  /** 准备执行的请求 */
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  /** 正在执行的异步请求,包含已经取消但未执行完的请求 */
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /** 正在执行的同步请求,包含已经取消单未执行完的请求 */
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

这里写图片描述

根据上面代码和思维脑图,很容易看到 Dispatcher是保存同步和异步Call的地方,并负责执行异步AsyncCall。

Dispatcher使用了一个Deque<AsyncCall> readyAsyncCalls保存了同步任务;
对于异步请求,Dispatcher使用了两个Deque,分别是Deque<AsyncCall> runningAsyncCalls Deque<RealCall> runningSyncCalls 一个保存准备执行的请求,一个保存正在执行的请求,为什么要用两个呢?因为Dispatcher默认支持最大的并发请求是64个,单个Host最多执行5个并发请求,如果超过,则Call会先被放入到readyAsyncCall中,当出现空闲的线程时,再将readyAsyncCall中的线程移入到runningAsynCalls中,执行请求。

注意同步请求调用Call的execute()方法,而异步请求调用call.enqueue()方法

我们先来看一下简单点的同步请求。

2.1 Dispatcher(任务调度器) 同步请求

执行同步请求,代码如下(RealCall的execute方法):

@Override public Response execute() throws IOException {  
    synchronized (this) {  
     //如果executed等于true,说明已经被执行,如果再次调用执行就抛出异常。这说明了一个Call只能被执行一次。
      if (executed) throw new IllegalStateException("Already Executed");  
      executed = true;  
    }  
    captureCallStackTrace();  
    eventListener.callStart(this);  
    try {  
      //关键代码在这
      client.dispatcher().executed(this);  
      Response result = getResponseWithInterceptorChain();  
      if (result == null) throw new IOException("Canceled");  
      return result;  
    } catch (IOException e) {  
      eventListener.callFailed(this, e);  
      throw e;  
    } finally {  
      client.dispatcher().finished(this);  
    }  
  }  

首先如果executed等于true,说明已经被执行,如果再次调用执行就抛出异常。这说明了一个Call只能被执行。client.dispatcher().executed(this)是我们的关键代码,我们来看一下Dispatcher源码中executed()方法。

synchronized void executed(RealCall call) {
   //正在执行的同步请求,包含已经取消单未执行完的请求  
   runningSyncCalls.add(call);  
 } 

源码太特妈简单了,就是将这个请求 call加入到runningSyncCalls队列中去。
在经过拦截器的处理之后,等一系列操作之后,得到了响应的Response,最终会执行finished()方法。来看看源码吧。

  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
  }

  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      //将请求从队列中移除
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      //同步请求 promoteCalls 为false 不会执行promoteCalls()方法
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

对于同步请求,只是简单的将同步请求从runningSyncCalls队列中移除,因为promoteCalls参数是false,因此不会执行promoteCalls方法,
Dispatcher中,同步请求的逻辑还是比较简单的,异步请求的逻辑相对复杂一点,一起来看看吧。

扫描二维码关注公众号,回复: 1063466 查看本文章

2.2 Dispatcher(任务调度器) 异步请求

异步请求调用RealCall.enqueue()方法,源码如下,

@Override public void enqueue(Callback responseCallback) {  
    synchronized (this) {  
      if (executed) throw new IllegalStateException("Already Executed");  
      executed = true;  
    }  
    captureCallStackTrace();  
    eventListener.callStart(this);  
    // 关键代码
    client.dispatcher().enqueue(new AsyncCall(responseCallback));  
  } 

如果当前的请求没有被执行过,则对当前请求添加监听器等操作,
来看看关键代码 client.dispatcher().enqueue(new AsyncCall(responseCallback)),首先生成Call(AsyncCall)对象,将Call(AsyncCall)对象作为参数传入Dispatcherenqueue
方法。来看看Dispatcherenqueue()源码吧。

synchronized void enqueue(AsyncCall call) {  
   // 执行的请求总数<=64 && 单个Host正在执行的请求<=5
   if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {  
     // 将请求加入到runningAsyncCalls队列中
     runningAsyncCalls.add(call);  
     // 在线程池中最终会调用AsyncCall的execute()方法
     executorService().execute(call);  
   } else {  
     // 将请求加入到readyAsyncCalls队列中
     readyAsyncCalls.add(call);  
   }  
 } 

代码注释也非常清楚,我们一起看看吧,如果正在执行的请求总数<=64 则 单个Host正在执行的请求<=5,则将请求加入到runningAsyncCalls队列中,紧接着就是利用线程池执行该请求,否则就将该请求放入readyAsyncCalls队列中。AsyncCall是RealCall的一个内部类并且继承NamedRunnable,NamedRunnable实现了Runnable接口并且是一个抽象类,在线程池中最终会调用AsyncCall的execute()方法执行异步请求。

 protected void execute() {  
     boolean signalledCallback = false;  
     try {  
       //拦截器链 
       Response response = getResponseWithInterceptorChain();
       //重试失败,回调onFailure方法   
       if (retryAndFollowUpInterceptor.isCanceled()) {
         signalledCallback = true;  
         responseCallback.onFailure(RealCall.this, new IOException("Canceled"));  
       } else {  
         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 {  
         eventListener.callFailed(RealCall.this, e);  
         responseCallback.onFailure(RealCall.this, e);  
       }  
     } finally {  
       //关键代码 结束
       client.dispatcher().finished(this);  
     }  
   }

我们主要看client.dispatcher().finished(this);其他逻辑和同步方法基本相同。

  void finished(AsyncCall call) {
    // 注意最后一个参数为true和同步的不一样
    finished(runningAsyncCalls, call, true);
  }

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      //将请求从队列中移除
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      // promoteCalls 为true
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

从上面代码中看到最后一个参数是true,这意味着需要执行promoteCalls()方法。

private void promoteCalls() {  
    // 正在执行的Call总数超过最大请求值直接返回
    if (runningAsyncCalls.size() >= maxRequests) return; 
    // readyAsyncCalls为空直接返回
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.  
    // 遍历执行readyRunningCalls的请求
    for (Iterator<AsyncCall> 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.  
    }  
  }  

promoteCalls的逻辑也很简单:扫描待执行任务队列,将任务放入正在执行任务队列,并执行该任务。

2.3 Dispatcher(任务调度器) 中线程池(ExecutorService)

OkHttp的一个高效之处在于在内部维护了一个线程池,方便高效地执行异步请求。

1 线程池概念
Android中的线程池的概念来源于Java中的Executor,Executor是一个接口,真正的线程池的实现为ThreadPoolExecutor,ThreadPoolExecutor提供了一系列参数来配置线程池,通过不同的参数可以创建不同的线程池。

2 线程池优点

(1)复用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。

(2)能够有效的控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。

(3)能够对线程进行简单的管理,并提供定时执行以及指定间隔循环执行等功能。

3 源码
在OkHttp,使用如下方法构造了单例线程池

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;
  }

//构造一个线程池ExecutorService:
executorService = new ThreadPoolExecutor(
//corePoolSize 最小并发线程数,如果是0的话,空闲一段时间后所有线程将全部被销毁
    0, 
//maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理
    Integer.MAX_VALUE, 
//keepAliveTime: 当线程数大于corePoolSize时,多余的空闲线程的最大存活时间
    60, 
//单位秒
    TimeUnit.SECONDS,
//工作队列,先进先出
    new SynchronousQueue<Runnable>(),   
//单个线程的工厂         
   Util.threadFactory("OkHttp Dispatcher", false));

这里写图片描述
构建了一个核心为[0, Integer.MAX_VALUE]线程数的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒,它使用了一个不存储元素的阻塞工作队列。
在实际运行中,当收到20个并发请求时,线程池会创建20个线程,当工作完成后,线程池会在60s后相继关闭所有线程。

三 总结

通过上述的分析,我们知道了:

  1. OkHttp采用Dispatcher(任务调度器)技术,与线程池配合实现了高并发,低阻塞的运行
  2. Okhttp采用Deque作为缓存,按照入队的顺序先进先出。
  3. OkHttp最出彩的地方就是在try/finally中调用了finished函数,可以主动控制等待队列的移动,而不是采用锁或者wait/notify,极大减少了编码复杂性。

猜你喜欢

转载自blog.csdn.net/zhangqilugrubby/article/details/80181373