OKHttp源码解析 3:任务调度核心类dispatcher解析

这一篇咱们来全面的对 Dispatcher 进行一次分析讲解,因为这是 OKHttp 的核心之一,所以这是很有必要的。前面的文章我们已经对 Dispatcher 进行了提及,这一篇我们就拨开迷雾,让 Dispatcher 清清楚楚的暴露在我们面前。

分析前的疑问及思考

首先,我们在还没开始分析源码之前,我们要在脑海里问自己几个问题:what、why、how。具体点就是:
- Dispatcher 到底是个啥东西?
- 它有啥用?
- 为什么要用它?
- 以及我们能从 Dispatcher 源码中借鉴到什么?

带着这些问题,我们开始 Dispatcher 的源码分析之旅。

从 OKHttp 的同步与异步请求入手

在前面我们对 OKHttp 的同步与异步请求的分析中,我们已经提到过 Dispatcher 这个分发器类,而且在最后还总结了一个流程图,现在我们看一下这个流程图
image
我们可以很明显的看到,OKHttp的所有 Call 请求,不管是同步请求 RealCall 还是异步请求 AsyncCall,下一步经过的都是 Dispatcher 这个类。从这里我们可以得出一个结论,那就是:它负责管理所有的 OKHttp 请求。

接下来我们看一下下面的源码

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

/** 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<>();

这段源码我们在上一篇文章中已经讲解过。很明显,Dispatcher 内部维护着三个队列:同步请求队列 runningSyncCalls异步请求队列 runningAsyncCalls异步缓存队列 readyAsyncCalls,和一个线程池 executorService

再看下面的源码段,在同步请求中,Dispatcher 只是把 Call 请求添加到同步请求队列中,非常简单有没有,Dispatcher 对同步请求就只是做了这一个操作,重点在异步请求。

//OKHttp的同步请求,只是单纯的把 Call 请求添加到同步请求队列中,交给线程池去执行
//线程不安全的,使用 synchronized 关键字锁住
synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

...

//异步请求方法,线程不安全的,使用 synchronized 关键字锁住
synchronized void enqueue(AsyncCall call) {
    //判断正在执行的异步请求是否小于允许的最大值、以及正在执行的异步请求的 Host 是否小于允许的最大值
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //条件允许,则把 Call 请求加入到正在执行的请求队列中
      runningAsyncCalls.add(call);
      //通知线程池执行这个请求
      executorService().execute(call);
    } else {
      //条件不允许,则把 Call 请求加入到异步等待队列中
      readyAsyncCalls.add(call);
    }
  }

在异步请求中,先是判断正在执行的异步请求队列 size 是否小于允许的最大值(64)、以及正在执行的异步请求队列 size的 Host 是否小于允许的最大值(5),如果为 true 就把当前的 Call 请求加入到异步请求队列中,为 false 的话,加入到 异步等待队列中。到这里,我们也就验证了前面说的观点:Dispatcher负责管理所有的 OKHttp 请求。

接下来,维护了请求之后,肯定还需要去执行这些请求,所以这个时候我们就要讲到线程池了。在上面的异步请求源码段中,是通过这段executorService().execute(call);去执行请求的,我们看一下 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;
  }

很明显,这里主要就是实例化线程池对象,注意前面三个参数的传值。前面三个参数的意思分别为:池最小值、池最大值、请求存活时间,这里分别传入的是:0、Integer 最大值、60。可能有人会对第二个参数的值有疑问,为什么要传入 Integer 最大值,这不是产生大量线程消耗资源嘛?其实并不会这样,我们要注意上面讲异步请求入队列的判断。就是当当前正在执行的异步请求队列的大小小于64时,才会加入到队列中,否则只会加入到等待队列中。所以线程池的池最大值传入 Integer 的最大值并不会造成大量资源占用,相反,还能方便我们根据实际情况自由调节请求队列的大小。

线程池我们弄清楚了,然后就是怎么去执行请求了。我们知道线程的运行会调用 run() 方法。上一篇我们已经提到过,AsyncCall 是一个 Runnable 的实现类,它的 run() 方法实现在 NamedRunnable 类中,这个 run() 方法又调用了一个抽象方法 execute(),而 AsyncCall 又实现了这个抽象方法。绕了一圈又绕了回来,所以最后的执行还是得看 AsyncCall 的 execute() 方法,源码如下:

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        //通过拦截链获取 Response
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          //返回 Callback 结果
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          //返回 Callback 结果
          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 {
        //主动finish掉请求
        client.dispatcher().finished(this);
      }
    }

源码我们在上一篇文章中也已经分析过了,主要就是通过拦截链获取 Response,然后根据判断返回 Callback,最后在 finally 中主动 finish 掉请求。讲到这里,我们已经把线程池、异步请求队列都讲到了,但是还有一个等待请求队列没有提到。那到底什么情况下才能使用到这个队列呢?我们大概猜想:当异步请求队列有空闲位置的时候,是不是就会把等待队列中的请求添加进去呢?我们来验证一下我们的想法,继续跟踪代码。

这个时候我们就要看一下,每次 finally 里面 finish 掉请求到底做了什么,我们看源码

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!");
      //当是异步请求调用 finish 方法时,会执行到这里
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

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

这段源码段我们在前面也分析过,但是有一段我们没有说明,那就是 promoteCalls() 这一句。在异步请求中,我们的 finish 方法才会传入 true,执行到这一句,这里我们来重点讲解一下这个方法,看源码

private void promoteCalls() {
    //当正在请求队列的 size 大于或等于允许的最大值时,直接返回
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    //当等待队列为空时,直接返回
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    //遍历等待请求队列
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      //当请求的 正在执行请求的host小于允许host的最大值时
      if (runningCallsForHost(call) < maxRequestsPerHost) {
        //把请求从等待队列中移除
        i.remove();
        //添加到请求队列中
        runningAsyncCalls.add(call);
        //线程池执行请求
        executorService().execute(call);
      }

      ////当正在请求队列的 size 大于或等于允许的最大值时,直接返回
      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

代码注释我已经写的非常详细了,很明显,这个方法就是起到了一个调整队列的作用,当异步请求队列有空闲位置时,就会从等待队列中添加一个请求到异步请求队列中,这样整个逻辑就完善了,Dispatcher 的主要内容也就都提及了。讲到这里, Dispatcher 是什么我们应该已经很清楚了,一句话总结:Dispatcher 通过维护一个线程池,来维护、管理、执行OKHttp的请求。

下一篇 OKHttp源码解析 4 - 1:拦截器源码分析、拦截器链

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值