OkHttp流程之Dispatcher

OkHttp的基本流程

android 開發大多用過Okhttp, 在使用過程中,大多也是同步異步兩種方式。一般使用方式如下(基於3.14.2版本):

public static final MediaType JSON
    = MediaType.get("application/json; charset=utf-8");

OkHttpClient client = new OkHttpClient();

String post(String url, String json) throws IOException {
       RequestBody body = RequestBody.create(json, JSON);
       Request request = new Request.Builder()
               .url(url)
               .post(body)
               .build();
       //同步
       try (Response response = client.newCall(request).execute()) {
           return response.body().string();
       }

       //異步
      client.newCall(request).enqueue(new Callback(){
        @Override
              public void onFailure(Call call, IOException e) {
              }

              @Override
              public void onResponse( Call call,  Response response) throws IOException {

              }
        });
   }
}

由此可見,無論同步異步其實整個請求大致可以分爲三個模塊, Request,Call,Response。它們就簡單概括了一個網絡請求的流程:封裝請求Request,進行網絡請求Call,返回結果Response. 所以我們在使用過程中只需要對這三個模塊進行封裝。

OkHttp的線程控制

  • client.newCall

雖然我們將一個網絡請求過程簡單的歸爲三個部分,但是其中的細節纔是我們需要清楚的。接下來看client.newCall的內容。

/**
 * Prepares the {@code request} to be executed at some point in the future.
 */
@Override public Call newCall(Request request) {
  return RealCall.newRealCall(this, request, false /* for web socket */);
}

newCall 方法返回了一個RealCall這個類實現了Call的接口, 可以認爲這個RealCall是Ok中執行網絡請求的一個單位。在RealCall中實現了一個請求的同步異步的兩個方法execute()和enqueue().

  • RealCall中的真正執行函數

我們看execute和enqueue的僞代碼(暫時不用的地方已經省略)

public Response execute() throws IOException {

    try {
      //調用okhttpclient中的分發器去執行
      client.dispatcher().executed(this);
      //將結果直接返回,無論同步異步最終都會調用到getResponseWithInterceptorChain
      //這個函數可以看做是整個okhttp的核心
      return getResponseWithInterceptorChain();
    } finally {
      //執行完需要調用分發器中的結束函數
      client.dispatcher().finished(this);
    }
  }

  public void enqueue(Callback responseCallback) {

    //同樣是用dispatcher進行分發,這裏是將AsyncCall進行執行
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

AysncCall是RealCall的內部類,這樣是爲了使用RealCall中的資源,比如okhttpclient,封裝的request等等。AsyncCall繼承了NameRunnable接口,NameRunable爲Runnable的實現,代碼如下:

public final void run() {
    //更換執行時候的線程名,主要是爲了調試的時候能根據name能明白線程的作用
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      //執行此類的抽象方法,NameRunable的子類需要實現這個方法
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

OkHttpClient中的Dispatcher

在上面的code中我們發現,無論是同步異步都會通過 client.dispatcher()這個對象去執行,看這個類名我們也可以猜測出這個類應該是負責線程的分發,可以說這個類算是okhttp的調度中心。既然是線程的分發,那麼肯定是涉及到了線程池。我們接下來看看Dispather的廬山真面目。

 //最大併發請求數,
 private int maxRequests = 64;
 // 同一個域名最大連接數是5
 private int maxRequestsPerHost = 5;

 //空閒線程,當dispatcher空閒的時候執行,類似Handler機制中的idleHandler,開發者可視情況選擇使用,本文不詳述
 private @Nullable Runnable idleCallback;

 //OkHttp 線程池
 private @Nullable ExecutorService executorService;

 //準備執行的保存異步Asyncall的有一個等待隊列
 private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

 //保存正在執行AsyncCall的隊列
 private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

 //正在執行的同步RealCall隊列
 private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

Dispatcher就是通過這些內容控制整個過程中線程的併發問題。首先來看線程池的配置

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      //核心線程爲0, 最大線程數爲MAX_VALUE理論上可以無限制的開闢線程,
      //空閒線程存活60s
      //SynchronousQueue爲有界隊列,大小爲0,只是進行生產消費的傳遞作用
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

其實看完發現和android提供的newCachedThreadPool很類似,只是改了線程名。我們可以簡單分析一下這個線程池的運行機制:

  • 線程池收到runnable因爲沒有核心線程,所以不需要創建核心線程。所以就直接加入任務隊列。
  • 因爲SynchronousQueue不保存Runnable,所有接收到線程,就會去檢查如果有空閒線程,直接傳遞給空閒線程,沒有的話就開闢一個新的線程去執行。

如果只是這樣就會存在一個很大的問題,如果一直這麼創建新的線程, 理論上就可以創建無數線程。 這顯然不合理。其實這只是okhttp調度線程的一部分,更重要的部分由readyAsyncCalls runningAsyncCalls runningSyncCalls這部分實現。無論是同步還是異步,httpclient封裝一個call傳遞給調度器。在上面提到的execute或者是enqueue函數我們追查下去都會最終到Dispatcher中的promoteAndExecute()方法中。我們以enqueue爲例。

Dispatcher.java

void enqueue(AsyncCall call) {
    synchronized (this) {
      //將傳遞進來的call保存進入readyAsyncCalls隊列
      readyAsyncCalls.add(call);
      //找出同域名的請求,如果存在,那麼已經存在的call中的callsPerHost數將會被共享,
      //callsPerHost數目表示同一個域名下鏈接的call的數目
      //callsPerHost爲AtomicInteger保證多線程原子操作性,並且保證線程可見爲volatile類型
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }

  private boolean promoteAndExecute() {
    //創建一個新的arraylist保存可執行的call
    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        //遍歷準備執行的隊列,如果有數據, 需要將數據視情況去執行並將可以執行的放到正在執行隊列
        AsyncCall asyncCall = i.next();
        //如果目前正在執行的call大於maxRequest默認是64,那麼直接停止退出循環
        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        //如果這次請求的域名下已經滿足最大連接個數(默認是5),那這個call暫時也不能執行
        if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

        i.remove();
        //將call中的表示域名連接數的變量自加1.
        asyncCall.callsPerHost().incrementAndGet();
        //將這個可以執行的call添加到executableCalls鏈表
        executableCalls.add(asyncCall);
        //同樣添加到正在執行的隊列
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    //遍歷executableCalls列表,將每個ayncall添加到線程池去執行
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());
    }

    return isRunning;
  }

由上面我們可以看出實際最大執行併發數是64, 並不會真的無限制的增加。最大併發線程數與每個域名同時可以連接數都是可以用戶自定義的。

Dispatcher.java
//設置最大併發線程數
public void setMaxRequests(int maxRequests)

//設置相同域名最大併發數
public void setMaxRequestsPerHost(int maxRequestsPerHost)

看到這裏我們已經清楚了,call是怎樣執行的,同時我們會有疑問,插入和移出必須是成對出現的,線程執行的時候添加到running隊列了。那麼什麼時候移出隊列呢?這就要我們繼續看AsyncCall的實際執行函數了。
AysncCall的父類NamedRunnable可以當做一個Runnable,從上文我們得知,在它的run方法中都需要執行 execute()方法,而這個方法是抽象方法,需要每個子類去實現。我們看看Ayncall中發生了什麼。

RealCall.AsyncCall

 protected void execute() {
      try {
        //獲取執行結果
        Response response = getResponseWithInterceptorChain();
        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 {
          //如果發生異常產生錯誤回調
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        //執行完之後,調用dispatcher中的finish函數
        client.dispatcher().finished(this);
      }
    }
  }

  //我們進一步看看Dispatcher中的finish函數
  Dispatcher.java

  //這個類重載了多個finished方法,最終調用到這個私有函數裏面
  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();
  //如果隊列沒有在執行的線程且idle線程不爲空則執行。 可以看出每個線程結束完都會檢查一遍是不是
  //要執行空閒線程
  if (!isRunning && idleCallback != null) {
    idleCallback.run();
  }
}

總結

以上就是okhttp大致的調度邏輯,本篇文章不涉及各種攔截器的分析。所以我們可以將請求的邏輯分爲三個模塊

  • 封裝Request。
  • 封裝RealCall提交到調度器Dispatcher去執行
  • 將結果返回
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章