OkHttp源碼閱讀(二)—— Dispatcher任務調度器

??上邊OkHttp源碼閱讀(OkHttp源碼閱讀(一)-——初識OkHttp)中,初步的了解了一個簡單http請求的執行流程,其中的一個關鍵角色就是Dispatcher,它主要負責請求的分發工作,下面具體分析一下它

同步任務分發

還記得上篇博客留下的坑嘛?那個標記異常醒目的重要代碼塊,如圖:

1.jpeg

我們已經知道同步請求實際上調用的是RealCall類中的execute方法,如上圖中所示的代碼是execute方法執行的主要部分,標記①中調用Dispatcher的execute方法將構建出來的Realcall對象傳入進去從而觸發同步執行,Dispatcher的execute方法調用其實很簡單,如下:

 /** Used by {@code Call#execute} to signal it is in-flight. */
  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

Dispatcher的execute方法只是將請求call添加到了runningSyncCalls中,這里有了一個新的角色runningSyncCalls,從字面上理解就是正在運行的同步請求組,實際上,它是一個隊列,正在執行的同步請求隊列,我們看一下它的初始化,也就是Dispatcher中runningSyncCalls的定義。

/** 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. */
  /**注意上邊的注釋,包括取消的但是還沒結束的請求call*/
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  /**注意上邊的注釋,包括取消的但是還沒結束的請求call*/
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

在Dispatcher中定義了三個隊列,分別是

  1. readyAsyncCalls 準備執行的異步請求隊列
  2. runningAsyncCalls 正在執行的異步請求隊列
  3. runningSyncCalls 正在執行的同步請求隊列

他們的定義都是使用Deque的方式,Deque(雙端隊列)的特性是集成了Queue(隊列)和Stack(棧)的特性,具體Deque的詳細介紹可以另一篇博客Java容器之雙端隊列Deque, 接下來執行Response result = getResponseWithInterceptorChain();獲取響應response,從源碼中可以看到從Request請求發出到獲取Response,所有的執行過程都是在主線程執行,所以同步請求會阻塞主線程,我們之前說過Call是鏈接Request和Response的一個中間橋梁,具體請求建立鏈接、參數傳遞等等包括獲取response的過程交給了連接器鏈InterceptorChain,暫且不贅述,后邊詳細說明,獲取到Response后,還有一個最最最重要的操作步驟 就是finally代碼塊,也就是標記③,調用Dispatcher的finished方法,具體源碼實現如下:

/** Used by {@code Call#execute} to signal completion. */
  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是個標記位,同步請求不會調用,異步請求會調用promoteCalls方法進行隊列重新整理*/
      if (promoteCalls) promoteCalls();
      /**獲取正在運行任務數*/
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }
    /**一下是閑置回調*/
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

??同步請求過程中的finished方法中,首先將runningSyncCalls正在執行的同步隊列中的元素移除,然后計算正在運行的任務數runningCallsCount,只有當正在執行的任務數為0的時候才會回調idleCallback,否則Dispachter還會繼續執行runningSyncCalls同步請求隊列中的其他任務或者是其他等待運行或者正在運行的異步任務, runningCallsCount()方法很簡單,就是判斷下當前正在執行的異步隊列和同步隊列的size之和。

public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

異步任務分發

??在實際開發過程中,大部分的網絡請求都是異步請求,在OKHttp中,異步任務的分發流程也比同步的要復雜些,首先還是直接從RealCall的enqueue()方法開始:

2.jpeg

同樣調用的是Dispatcher的enqueue方法,與execute方法不同的是,enqueue方法傳入的是AsyncCall的對象,跟蹤源碼可以看到AsyncCall實際上是一個Runnable對象,

final class AsyncCall extends NamedRunnable 
**
**
***
**

public abstract class NamedRunnable implements Runnable

接下來看一下Dispatcher的enqueue()方法具體執行流程:

synchronized void enqueue(AsyncCall call) {
    /**當前運行的異步任務數小于最大請求數,并且每個主機的請求數小于每個主機最大請求數*/
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      /**將改請求任務放入到正在執行的任務隊列中*/
      runningAsyncCalls.add(call);
      /**線程池執行該任務*/
      executorService().execute(call);
    } else {
      /**如果不滿足上邊條件,則該任務會被加入到緩存隊列,也就是準備執行的任務隊列*/
      readyAsyncCalls.add(call);
    }
  }

enqueue方法對runningAsyncCalls和readyAsyncCalls隊列進行的操作,由于兩個隊列都非線程安全隊列,所以enqueue方法被加上了synchronized,首先判斷條件當前運行的異步任務數小于最大請求數,并且每個主機的請求數小于每個主機最大請求數時,該任務會直接加入到正在執行的異步任務隊列runningAsyncCalls中,否則該任務會暫時被加入到緩存隊列readyAsyncCalls中,Dispatcher中默認最大請求數 maxRequests是64個,每個主機最大請求數 maxRequestsPerHost是5個 ,另外Dispatcher還維護了一個線程池ExecutorService,并通過上面executorService()進行初始化:

  private int maxRequests = 64;
  private int maxRequestsPerHost = 5;
  private @Nullable Runnable idleCallback;

  *
  *
  *
  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在初始化時,將線程池的最大線程個數設置為Integer.MAX_VALUE,理論上說正在運行的請求線程是Integer.MAX_VALUE個,但是Dispatcher中做了默認最大請求數maxRequests的限制,所以在用戶沒有特殊指定最大請求數時,Okhttp同時運行的請求個數不會超過64個。

AsyncCall

??之前提到AsyncCall是個Runnable,上述異步任務執行過程中,當ExecutorService執行execute()方法時就是調用AsyncCall的run方法,由于AsyncCall繼承NamedRunnable關系,實際上調用的是AsyncCall的execute方法,如下:

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    *
    *
    *
    
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        /**通過攔截器鏈獲取Response*/
        Response response = getResponseWithInterceptorChain();
        /**重試和重定向攔截器是否被取消*/
        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);
      }
    }
  }

上邊AsyncCall源碼可以看出還是通過攔截器鏈獲取Response,然后通過一些判斷返回回調,這里攔截器鏈后面會詳細介紹,重點是還是finally代碼塊,同步請求中Dispatcher的finished方法和異步的有所差別。

/** Used by {@code AsyncCall#run} to signal completion. */
/**異步請求調用此方法*/
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }

  /** Used by {@code Call#execute} to signal completion. */
  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是個標記位,同步請求不會調用,異步請求會調用promoteCalls方法進行隊列重新整理*/
      if (promoteCalls) promoteCalls();
      /**獲取正在運行任務數*/
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }
    /**一下是閑置回調*/
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

異步請求調用了promoteCalls標記位為true的方法,前面的隊列remove方法不變,重點在promoteCalls()方法,這個方法Dispatcher會重新整理緩存隊列和正在執行的任務隊列,如下:

private void promoteCalls() {
    /**正在運行的任務數越界*/
    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();
      /**遍歷緩存隊列中等待執行的任務,判斷條件是每臺主機最大請求數小于最大限制*/
      if (runningCallsForHost(call) < maxRequestsPerHost) {
        /**滿足上邊條件的任務會從緩存隊列中取出,并放入到正在執行的任務隊列中,然后線程池執行該任務*/
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }
      /**每次正在執行任務隊列在添加時都要判斷是否已經超過指定最大請求任務數*/
      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

可以看出,異步任務在調用Dispachter的finished()方法時,會循環的獲取緩存隊列中的任務,只要有任務并滿足限制條件就會被執行(AsyncCall.execute()方法),然而AsyncCall.execute()方法執行完后finally又會執行Dispachter的finished()方法,如此循環調用直到正在執行的任務隊列runningAsyncCalls沒有任務為止,最后拋出idleCallback回調,處于閑置狀態! 緩存隊列和正在執行的任務隊列二者之間的關系有點像生產者和消費者模式, Dispachter負責生產 ExecutorService負責消費

總結

??同步和異步請求區別除了Call調用的方法不同以外,Dispatcher的處理機制也不相同,同步請求時Dispatcher會直接將這個任務丟進正在執行的同步任務隊列中,直到獲取到Response后,再從隊列中移除該任務,所以同步請求會阻塞當前線程。異步任務請求時Dispatcher會判斷當前的執行任務條件,選擇放入緩存任務隊列還是正在執行的異步任務隊列,通過自己維護的線程池執行異步任務,得到Response,執行完成以后循環判斷緩存隊列和執行隊列任務數,最終將所有任務執行完成,由于執行任務和獲取Response都是在子線程中完成,所以異步任務不會阻塞當前線程

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。