OkHttpClient原始碼分析(一)—— 同步、非同步請求分析和Dispatcher的任務排程

chaychan發表於2018-07-25

OkHttpClient同步請求的執行流程和原始碼分析

同步請求示例

OkHttpClient okHttpClient = new OkHttpClient.Builder().readTimeout(5, TimeUnit.SECONDS).build();
        Request request = new Request.Builder().url("https://www.baidu.com").get().build();
        Call call = okHttpClient.newCall(request);
        try {
            Response response = call.execute();
            Log.e(TAG,"response: " + response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
複製程式碼

同步請求的步驟

  1. 建立OkHttpClient物件和Request物件,均是採用Builder模式建立,構建者(Builder)設計模式(又叫生成器設計模式)

  2. 將Request封裝成Call物件

  3. 呼叫Call的execute()方法傳送同步請求,傳送請求後,就會進入阻塞狀態,直到收到響應。

一、(1)OkHttpClient Builder物件分析

public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
}
複製程式碼

  OkHttpClient Builder的建構函式,主要是對一些引數賦值預設值,對一些物件進行初始化,Dispatcher是OkHttpClient中http請求的分發器,由它來決定非同步請求是直接處理還是進行快取等待,對於同步請求,它並沒有做太多操作,只是把同步請求放到佇列當中去執行。ConnectionPool是一個連線池物件,用於管理連線物件,當存在同樣的Url請求時,可以複用,從連線池中找到對應快取的連線物件。

(2)Request 物件分析

 public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder();
}
複製程式碼

Request Builder的建構函式,預設請求方法為GET,同時初始化一個Header物件。

 public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
}
複製程式碼

build()方法是建立Request物件,將當前的builder物件傳入,接下來看Request的建構函式:

Request(Builder builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.tags = Util.immutableMap(builder.tags);
  }
複製程式碼

  可以看到,是將傳入的Builder物件中的屬性賦值給Request的相關屬性,這樣就建立好了Request物件。

二、建立Call 物件分析

/**
   * 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 */);
  }
複製程式碼

  OkHttpClient 物件中的newCall()方法,返回值是一個Call物件(介面),在這裡可以看到實際上呼叫的RealCall.newRealCall()方法建立,RealCall是Call介面的一個實現類,接著檢視RealCall類中newRealCall()方法:

  static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
  }
複製程式碼

  可以看到newRealCall()方法中建立了Call介面的實現類RealCall物件並返回該物件,到此Call物件的建立便完成了。

三、Call 物件exexcute()方法分析

  上面有提及到Call物件是一個介面,我們點選檢視exexcute()方法時,需要點選檢視該方法的實現,實際上是進入到RealCall物件的exexcute()方法:

@Override public Response execute() throws IOException {
    synchronized (this) {
      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標識,如果當前已經有在執行,則會丟擲"Already Executed"資訊的異常,如果沒有執行過,則更改executed標識為true。

  接著呼叫captureCallStackTrace()方法,這個方法主要用於捕捉一些http請求的異常堆疊資訊。

  eventListener.callStart(this)開啟事件監聽,通過檢視該方法:

 /**
   * Invoked as soon as a call is enqueued or executed by a client. In case of thread or stream
   * limits, this call may be executed well before processing the request is able to begin.
   *
   * <p>This will be invoked only once for a single {@link Call}. Retries of different routes
   * or redirects will be handled within the boundaries of a single callStart and {@link
   * #callEnd}/{@link #callFailed} pair.
   */
  public void callStart(Call call) {
  }
複製程式碼

  通過閱讀該方法的註釋,可以知道該方法會在呼叫Call物件的enqueue()或execute()方法的時候,就會開啟這個listener。

接下來分析一下這個方法中的核心程式碼:

client.dispatcher().executed(this);
複製程式碼

首先呼叫OkHttpClient的dispatcher()方法

public Dispatcher dispatcher() {
    return dispatcher;
}
複製程式碼

該方法返回一個Dispatcher物件,緊接著呼叫該物件的executed()方法:


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

  該方法中,runningSyncCalls是一個存放同步請求的佇列,這裡僅僅只是將RealCall加入到同步請求的佇列中,Dispatcher物件中相關的佇列有:

 /** 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<>();
複製程式碼
  • readyAsyncCalls 是非同步請求的就緒佇列
  • runningAsyncCalls 是非同步請求的執行佇列
  • runningSyncCalls 是同步請求的執行佇列

  呼叫完Dispatcher的executed()方法後,緊接著呼叫getResponseWithInterceptorChain()方法獲取Response物件,這個其實是一個攔截器鏈的方法,該方法內部會依次呼叫攔截器來進行相應的操作。

最後看一下finally中:

finally {
      client.dispatcher().finished(this);
}
複製程式碼

  通過呼叫Dispatcher的finished()方法,傳入當前的RealCall物件,檢視該方法的程式碼可以發現:

  /** 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!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }
複製程式碼

  該方法繼續呼叫了其他一個同名的的方法,將正在執行的同步請求佇列傳了進來,在同步程式碼塊中,移除掉同步請求佇列中的call物件,並進行了判斷,如果移除出錯,則會丟擲異常。接著判斷promoteCalls,由於這裡傳入的promoteCalls為false,所以不會走promoteCalls()方法。

  接著,對runningCallsCount重新賦值,runningCallsCount用於記錄當前正在執行的請求數,檢視該方法的程式碼:

public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
}
複製程式碼

該方法很簡單,即返回正在執行的非同步請求數和正在執行的同步請求數的總和。

if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
}
複製程式碼

  最後通過判斷當前正在執行的請求數,如果當前沒有正在執行的請求數並且有設定閒置時的回撥,則會回撥其run()方法。

總結

  到此,同步請求的執行流程就已經分析完了,由上述的分析可以知道,在同步請求中,Dispatcher分發器做的工作非常簡單,就兩個操作,儲存同步請求和移除同步請求

OkHttpClient非同步請求的執行流程和原始碼分析

非同步請求示例

 OkHttpClient okHttpClient = new OkHttpClient.Builder().readTimeout(5, TimeUnit.SECONDS).build();
        Request request = new Request.Builder().url("https://www.baidu.com").get().build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                
            }

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

            }
        });
複製程式碼

非同步請求的步驟

  1. 建立OkHttpClient物件和Request物件,均是採用Builder模式建立,構建者(Builder)設計模式(又叫生成器設計模式)

  2. 將Request封裝成Call物件

  3. 呼叫Call的enqueue()方法進行非同步請求

同步和非同步的區別

  1. 發起請求的方法呼叫

  2. 阻塞執行緒與否

原始碼分析

  非同步請求的前兩步,和同步請求的一致,都是一些準備工作,並沒有發起請求,這裡不再重複說明,最主要的是第三步,呼叫Call物件的enqueue()方法,具體的實現還是在RealCall類中,檢視該方法程式碼:

  @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));
  }
複製程式碼

  前面的操作和同步請求的execute()方法相似,主要是 client.dispatcher().enqueue(new AsyncCall(responseCallback)) 這行程式碼,呼叫Dispatcher的enqueue()方法,將Callback回撥封裝成AsyncCall物件作為引數傳入,通過檢視程式碼,瞭解到AsyncCall物件繼承自NamedRunnable物件,而NamedRunnable物件實現了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);
    }
  }
複製程式碼

  該方法前加了synchronized修飾符,是一個同步方法,根據判斷當前執行的非同步請求數是否小於maxRequests(最大請求數,預設為64) 且當前執行的非同步請求佇列中相同主機的請求數小於maxRequestsPerHost(每個主機最大請求數,預設為5) 來進行處理,如果二者都小於設定的值,則將該請求新增到runningAsyncCalls(非同步請求執行佇列)中,否則則新增到readyAsyncCalls(非同步請求準備佇列)中。

runningCallsForHost()方法的程式碼:

 /** Returns the number of running calls that share a host with {@code call}. */
  private int runningCallsForHost(AsyncCall call) {
    int result = 0;
    for (AsyncCall c : runningAsyncCalls) {
      if (c.get().forWebSocket) continue;
      if (c.host().equals(call.host())) result++;
    }
    return result;
  }
複製程式碼

  通過註釋可以知道,該方法返回同一個主機的請求數目,通過遍歷執行中的非同步請求佇列,和傳入的AsyncCall物件的主機對比,如果相同則記錄數遞增,以此獲得和傳入AsyncCall物件相同主機的請求數。

enqueue()方法中,主要的程式碼:

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;
}
複製程式碼

  該方法也是一個同步方法,主要用於返回 ExecutorService 物件,在這裡僅一次建立了執行緒池物件 ThreadPoolExecutor,第二個引數傳入了Integer的最大值,即執行緒池所能容納的最大執行緒數為Integer.MAX_VALUE,雖然這裡設定了很大的值,但是實際情況下並非會達到最大值,因為上面enqueue()方法中有做了判斷,主要的還是maxRequests這個值決定非同步請求執行緒池的最大數量。

  executorService()方法返回了執行緒池物件,接著呼叫它的execute()方法,傳入實現Runnable介面的AsyncCall物件,上面提及到AsyncCall繼承NamedRunnable,而NamedRunnable物件實現了Runnable介面,所以我們想知道該執行緒池執行這個任務做了什麼,就得看下NamedRunnable物件的 run() 方法:

@Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
}

複製程式碼

該方法中,真正的處理邏輯是在execute()方法中:

 protected abstract void execute();
複製程式碼

而execute()方法是一個抽象方法,所以要回到繼承NamedRunnable物件的AsyncCall類中:

 @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        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);
      }
}
複製程式碼

  這裡才是真正進行非同步請求操作的邏輯,同樣也是通過getResponseWithInterceptorChain()方法得到Response物件,關於getResponseWithInterceptorChain()方法的分析在下面的文章裡將會介紹,接著通過判斷retryAndFollowUpInterceptor是否取消回撥CallBack介面的onFailure()或onResponse()方法,最後finally中,和同步請求的處理一樣,呼叫了Dispatcher物件的finished()方法:

 /** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, 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!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
}
複製程式碼

這裡的處理和同步請求結束後的處理多了一個promoteCalls()方法的呼叫,因為這裡promoteCalls傳入了true,所以會走promoteCalls()方法:

  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.
    }
  }
複製程式碼

  看完這個方法,會有一種恍然大悟的感覺,因為上面呼叫enqueue()方法的時候,會根據情況將請求新增到runningAsyncCalls(非同步請求執行佇列)或readyAsyncCalls(非同步請求準備佇列)中,而readyAsyncCalls佇列中的請求什麼時候執行呢,相信在看enqueue()方法的時候會有這個疑問,看了promoteCalls()後疑問將會被解答,為了方便閱讀再次貼上enqueue()方法:

  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }
複製程式碼

  promoteCalls()方法中,首先做了一些判斷,當runningAsyncCalls(非同步請求執行佇列)已經達到設定的最大的請求數或當前readyAsyncCalls(非同步請求準備佇列)中沒有請求的時候,則直接返回不做處理,如果滿足條件,則會遍歷readyAsyncCalls佇列,將該請求新增到runningAsyncCalls佇列中,並呼叫 executorService().execute(call) 對該請求進行處理。

總結

  如果非同步請求數超過最大請求數或同個主機最大請求數超過設定的值的時候,該請求就會新增到readyAsyncCalls(非同步請求準備佇列)中,當執行完runningAsyncCalls(非同步請求執行佇列)的請求後,將會呼叫Dispatcher的finished()三個引數的方法,第三個引數傳入true,會呼叫promoteCalls()方法,遍歷準備佇列readyAsyncCalls,將該佇列的中的請求新增到執行佇列runningAsyncCalls中,呼叫 executorService().execute(call)進行處理。

Dispatcher的作用

維護請求的狀態,並維護一個執行緒池,用於執行請求。

非同步請求為什麼需要兩個佇列

非同步請求的設計可以將其理解成生產者消費者模式,其中各個角色分別為:

  • Dispatcher 生產者
  • ExecutorService 消費者池
  • Deque readyAsyncCalls 快取
  • Deque runningAsyncCalls 正在執行的任務

當同步和非同步請求結束後,會呼叫dispatcher的finished方法,將當前的請求從佇列中移除。

下一篇文章中,將為大家講解一下OkHttp的攔截器鏈,感興趣的朋友可以繼續閱讀:

OkHttpClient原始碼分析(二) —— RetryAndFollowUpInterceptor和BridgeInterceptor

相關文章