OkHttp3.0(三)-Dispatcher分配器

1.概述

我们在上一节OkHttp3.0(二)-OkHttpClient类、Request类、Call类、Response的简单分析,学习了OkHttp在基本使用的时候,经常会用到的及各类,其中提到了一个类Dispatcher分发器,其实关于Dispatcher我们在上节课基本上已经说了很多,由于个人觉得该类对我们学习OkHttp的源码帮助很大,所以认为有必要将其单独拿出来进行分析讲解。

当调用者使用Call发送同步或者异步请求的时候,我们都看到了使用Dispatcher,而Dispatcher分发器,是OkHttpClient初始化的时候,就已经实例化了的,对于同步请求和异步请求,同步请求和异步请求的区别,主要就是内部Dispatcher的处理方式的不同。我们一定还记得,上一章节,我们提到的Dispatcher针对同步请求和异步请求所做的事情:限制最大并发量、限制同主机最大请求数、使用队列添加或者删除请求、线程池缓存异步请求。我们通过如下的一幅图来大概了解一下Dispatcher大概做的事情

2.Dispatcher基本解析

2.1.对并发量、同主机请求量的控制

通过上一章节的讲解,我们知道,Dispatcher内部维护着三个请求队列,同时有两个int变量控制最大并发量以及同主机最大请求数。我们还是通过代码看一下

  //允许同时被执行的最大请求数,默认为64
  private int maxRequests = 64;
  //相同主机下,允许运行的最多请求数,默认为5
  private int maxRequestsPerHost = 5;

  //准备(等待)执行的异步请求队列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  //正在执行的异步请求队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //正在执行的同步请求队列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
  //如果当前Dispatcher没有正在执行的请求,则就是出于闲置状态,会调用idleCallback的run方法  
  private @Nullable Runnable idleCallback;

我们可以注意到,对于异步请求来说,拥有两个队列,一个用来维护正在执行的异步请求,另一个用来维护正在等待的异步请求,而同步请求只有一个正在执行的同步请求队列在维护。所以当我们发送同步请求,会立即被Dispatcher分发器加入到runningSyncCalls队列,并且会立即向服务器发送Http请求,不论请求是否成功,方法执行完后一定会从runningSyncCalls队列中把该请求移除掉。我们再次通过代码看一下:

RealCall的execute()同步请求

 //程序员调用此方法发送同步请求 
 @Override public Response execute() throws IOException {
    ...
    try {
      //将请求加入到Dispatcher的runningSyncCalls队列中
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      ...
      //将请求结果返回给调用者
      return result;
    } catch (IOException e) {
      ...
    } finally {
      //将请求从Dispatcher的runningSyncCalls队列中移除
      client.dispatcher().finished(this);
    }
  }

我们分别在看一下Dispatcher的execute(RealCall)和finished(RealCall)方法:

  //将同步请求加入到runningSyncCalls队列
  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }
  //被RealCall的同步请求调用
  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
  }
  //当是同步请求的时候,此方法做的只是从runningSyncCalls队列中奖同步请求移除
  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      //将请求从calls中移除(calls就是runningSyncCalls)
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls)//同步请求,promoteCalls为false
          promoteCalls();
      //正在执行的请求(正在执行的同步请求+正在执行的异步请求)
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

那么我们此时应该有疑问:既然同步请求并没有作缓存,每次被调用就直接执行,何必多此一举,做存入队列、从队列移除这样的操作呢?我们看到上面的代码中,有runningCallsCount = runningCallsCount();代码,根据字面意思可以猜出,这里获取到的是正在执行的所有请求(同步+异步),我们不妨看一下runningCallsCount()方法的代码:

  //正在执行的同步请求数量+正在执行的异步请求数量
  public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

我们在结合前面上面代码中提到过的idleCallback变量,我们在分析下如下代码:

  //如果当前Dispatcher没有正在执行的请求,则就是出于闲置状态,会调用idleCallback的run方法,一般为null  
  private @Nullable Runnable idleCallback;
  
  public synchronized void setIdleCallback(@Nullable Runnable idleCallback) {
    this.idleCallback = idleCallback;
  }
  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
   ...
    Runnable idleCallback;
    synchronized (this) {
      ...
      runningCallsCount = runningCallsCount();//获取当前Dispatcher正在执行的所有请求数
      idleCallback = this.idleCallback;//给idleCallback 赋值
    }
    //如果当前Dispatcher没有正在执行的请求,就会调用idleCallback的run方法
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

我们回忆下在将OkHttpClient的时候,OkHttpClient提供了对Dispatcher的设置,我们可以通过OkHttpClient.Bulider提供的方法,设置或者获取默认初始化好的Dispatcher,然后调用Dispatcher的setIdleCallback(Runnable)方法,这就意味着调用者持有Runnable接口的回调run()方法。再结合上面代码,我们可以明白,每执行完一次请求调用Dispatcher的finished()方法时候,会判断当前Dispatcher维护的请求队列中,正在执行的请求数量是否为0,及是否处于闲置状态。如果当前Dispatcher某时刻处于闲置状态,而且调用者设置idleCallback参数,则调用者会接收到run方法回调,从而做一些相应的操作。

话题被岔开了,我们言归正传继续来看下Dispatcher是如何控制并发量、同主机请求数的控制。其实Dispatcher针对同步请求,主要就是利用它来计数,判断当前是否为空闲状态。对并发量的限制、同主机请求数的限制,就是针对异步请求而言的,还记得我们上一章节讲到的,我们调用了Call的enqueue(Callback)方法,Call就会调用Dispatcher的enqueue(AsyncCall)方法,所以我们再次看下Dispatcher的enqueue(AsyncCall)方法:

synchronized void enqueue(AsyncCall call) {
    //正在执行的异步请求数小于最大并发量&&与当请求相同HOST的请求数量小于允许的最大数
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //将异步请求加入到正在执行的异步请求队列
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {//否则将异步请求加入到正在等待的队列
      readyAsyncCalls.add(call);
    }
  }

这里就是关键,在执行异步请求的时候,会先判断当前Dispatcher请求队列的情况,正在执行的异步请求数量是否达到了允许同时执行的最大请求数(默认为64)、正在执行的与当前请求相同HOST的请求数是否达到了允许的最大同主机执行数(默认为5),通过这个判断,来决定异步请求是会被立即执行,还是进入等待队列缓存。当然我们可以同过Dispatcher设置最大请求数和最大同主机请求数:

//设置同时被允许的最大异步请求数 
public synchronized void setMaxRequests(int maxRequests) {
    if (maxRequests < 1) {
      throw new IllegalArgumentException("max < 1: " + maxRequests);
    }
    this.maxRequests = maxRequests;
    promoteCalls();
  }
//设置同主机被允许的最大请求数
 public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
    if (maxRequestsPerHost < 1) {
      throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
    }
    this.maxRequestsPerHost = maxRequestsPerHost;
    promoteCalls();
  }

2.2.Dispatcher对异步请求的缓存、执行

通过前面的讲解,我们可以知道的是,Dispatcher并没有对同步请求做控制,异步请求则不然,会受到控制。满足当前条件,则会被加入正在执行的异步请求队列,并且被执行;不满足条件则会被加入到等待执行的异步请求队列,等待着Dispatcher的分发执行任务。那么,被缓存到等待队列的异步请求,什么时候会被再次判断并且调用呢?异步请求又是如何实现异步的呢?给等待的异步请求队列分配执行任务之后,队列之间该如何控制?又是如何同时满足正在执行的异步请求数量没有超出最大并发量、正在执行的同主机异步请求数量没有超出范围?我们接下来进行分析之:

用来存储异步请求的队列有两个,一个是用来存储正在执行的异步请runningAsyncCalls ,另一个是用来存储准备执行(等待状态)的异步请求readyAsyncCalls。其实原理就是这样子:我们会在某一时刻判当前正在执行的请求数量是否小于允许的最大并发量,如果小于,那么,就从readyAsyncCalls队列中移出一个请求,存入runningAsyncCalls 队列中,并且给其一个异步线程让其执行,执行完毕之后,再将其从runningAsyncCalls队列中删除;然后继续判断、取出、存入、执行、删除等操作。Dispatcher对异步请求的控制,其一是在enqueue(AsyncCall)方法中,其二是在promoteCalls()方法中,enqueue(AsyncCall)方法,我们刚刚已经讲过,主要是对并发量、同主机请求量的控制,判断是否立即执行当前异步请求,如果是则给它异步线程并且执行。promoteCalls()方法是我们接下来要讲的,从方法的字面意思来说,是“促进请求执行”的意思,我们刚才提出一连串问题,其中说到如何让处于等待状态的异步请求进入执行状态,其关键就在这个方法中,我们分析之:

 //推进异步请求的执行
 private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) //正在执行的异步请求数大于最大值,返回
        return; 
    if (readyAsyncCalls.isEmpty())//没有处于等待状态的异步请求,返回
        return; // No ready calls to promote.
    //遍历等待状态的异步请求队列
    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; //循环过程中,每次都要判断
    }
  }

promoteCalls()方法,究竟在什么地方调用了呢?我们找了出来,并将相关代码放到这儿看下:

 public synchronized void setMaxRequests(int maxRequests) {
    ...
    this.maxRequests = maxRequests;
    promoteCalls();
  }
 public synchronized void setMaxRequestsPerHost(int maxRequestsPerHost) {
    ...
    this.maxRequestsPerHost = maxRequestsPerHost;
    promoteCalls();
  }
 private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    ...
    synchronized (this) {
      ...
      if (promoteCalls)
        promoteCalls();
      ...
    }
   ...
  }

没错,只有这三个地方调用了promoteCalls()方法,在异步请求结束之后执行finished()方法、在设置最大并发量、在设置同主机最大请求数这三个地方,会调用promoteCalls()方法,做了这几件事情去:1.在执行之前,判断了是否满足最大并发量和最大同主机请求量;2.维护了readyAsyncCalls和runningAsyncCalls队列的请求存储的转移;3.分配异步线程执行了符合条件的异步请求。

我们前面总是提到,把异步请求放在异步线程中去执行,那么具体又是如何做到的呢?我们先看下每次真正执行异步请求的代码

executorService().execute(call);

我们在看一下executorService()方法是什么

/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;

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

没错,就是线程池,ThreadPoolExecutor,我们稍微解释下ThreadPoolExecutor构造函数的这几个参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
  ...
}

corePoolSize:核心线程数,如果运行的线程少于corePoolSize,则创建新线程来执行新任务,即使线程池中其他线程是空闲的

maximumPoolSize:最大线程数,可允许创建的线程数,corePoolSize和maximumPoolSize设置的边界自动调整池大小:

   corePoolSize<运行的线程数<maximumPoolSize,仅当队列满时,才创建新线程;

   corePoolSize=运行的线程数=maximumPoolSize,创建固定大小的线程池

keepAliveTime:如果线程数多余corePoolSize,则这些多余的线程空闲的时间超过keepAliveTime时被终止

unit:keepAliveTime时间的单位

workQueue:保存任务的阻塞队列,与线程池的大小有关:

   当运行的线程数小于corePoolSize时,有新的任务则直接创建新的线程执行任务而不需要存入队列;

   当运行的线程数大于等于corePoolSize时,有新任务则加入队列,不会直接创建线程;

   当队列满时,有新任务就创建新的线程

threadFactory:使用threadFactory创建新线程,默认使用DefaultThreadFactory创建线程

handler:定义处理被拒绝任务的策略,默认使用ThreadPoolExecutor.AbortPolicy,任务被拒绝时抛出RejectedExecutionExecption

那么问题来了:我们的OkHttp的Dispatcher中,线程池的构造函数,分别设置corePoolSize和maximumPoolSize的值为0和Integer.MAX_VALUE,核心线程数为0可以理解,避免每次新任务都创建新线程浪费内存,最大线程数为最大整数,意味着允许被创建的线程数没有限制,不考虑内存性能,那岂不是很不严谨吗?其实根本不可能,因为我们的Dispatcher中已经定义了最大并发数和同主机同时发送请求的最大数,并且给客户提供了设置这俩值的方法。

  private int maxRequests = 64;
  private int maxRequestsPerHost = 5;
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

也就是说,最大在极限条件下,runningAsyncCalls 的大小不被允许超出64,这也就限制了Dispatcher中线程池的大小。

Dispatcher通过executorService().execute(AsycCall)的方法,执行异步请求。ThreadPoolExecutor线程池会将execute(Runnable)方法传入的任务放到线程中执行,这就实现了异步。我们没有看到Runnable是因为AsycCall的父类NamedRunnable实现了Runnable,并且在run方法中调用了自身的抽象方法execute()。这些我们在上一章节中详细讲解过了。

3.Dispatcher的其他功能

3.1.取消所有请求

Dispatcher还有其他的功能,取消所有请求(包括正在执行的同步请求、正在执行的异步请求、等待执行的异步请求)

  /**
   * Cancel all calls currently enqueued or executing. Includes calls executed both {@linkplain
   * Call#execute() synchronously} and {@linkplain Call#enqueue asynchronously}.
   */
  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中的cancle方法

  @Override public void cancel() {
    retryAndFollowUpInterceptor.cancel();
  }

执行了重定向拦截器的cancle方法,我们现在只需要知道,这是取消请求的方法(关闭Socket)就可以了,我们后面在讲拦截器的时候,会讲解。我们可以通过OkHttpClient的dispatcher().cancelAll()的方式,取消掉当前OkHttpClient的Dispatcher维护的所有请求。

3.2.获取请求队列

 //返回当前等待执行的异步请求列表(该列表只读)
 public synchronized List<Call> queuedCalls() {
    List<Call> result = new ArrayList<>();
    for (AsyncCall asyncCall : readyAsyncCalls) {
      result.add(asyncCall.get());
    }
    return Collections.unmodifiableList(result);
  }
  //返回当前正在执行的异步请求列表(该列表只读)
  public synchronized List<Call> runningCalls() {
    List<Call> result = new ArrayList<>();
    result.addAll(runningSyncCalls);
    for (AsyncCall asyncCall : runningAsyncCalls) {
      result.add(asyncCall.get());
    }
    return Collections.unmodifiableList(result);
  }
  //返回当前正在等待执行的异步请求数量
  public synchronized int queuedCallsCount() {
    return readyAsyncCalls.size();
  }
  //返回所有正在执行的请求数量(正在执行的同步请求+正在执行的异步请求)
  public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

4.总结

1.我们可以使用OkHttpClient的Dispatcher的setIdleCallback(Runnable)方法,监控当前OkHttpClient是否处于闲置状态(没有正在执行的同步或者异步请求)。

2.Dispatcher对同步请求和异步请求的处理方式不同,当Call发送同步请求时,Dispatcher并未干涉,Call会立即执行同步请求;当Call发送异步请求时,Dispatcher会根据当前正在运行的异步请求数和与当前同主机的正在运行的异步请求数量,来控制当前异步是等待还是执行。

3.Dispatcher内部维护了三个队列,正在执行的同步请求runningSyncCalls、正在执行的异runningAsyncCalls、处于等待状态的异步请求readyAsyncCalls,同时维护了两个最大值,允许被同时执行的最大请求数maxRequests、同主机允许最大请求数maxRequestsPerHost,从而实现了Dispatcher本身的最大用处:对异步请求的分配作用。

4.Dispatcher内部维护了一个线程池executorService,用来实现异步请求的缓存以及异步功能。

5.Dispatcher对外提供了取消当前OkHttpClient所有请求任务的功能。

 

 

 

 

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值