OkHttp 源碼解析(一):基本流程

簡介

OkHttp 是一款用于 Android 和 Java 的網絡請求庫,也是目前 Android 中最火的一個網絡庫。OkHttp 有很多的優點:

  • 在 HTTP/2 上允許對同一個 host 的請求共同一個 socket
  • 連接池的使用減少請求延遲(如果 HTTP/2 不支持)
  • 透明的 GZIP 壓縮減少數據量大小
  • 響應的緩存避免重復的網絡請求

之前寫過一篇 Retrofit 源碼解析,Retrofit 底層其實就是用的 OkHttp 去請求網絡。本文分析 OKHttp 的源碼,主要是針對一次網絡請求的基本流程,源碼基于 OKHttp-3.8.0

基本用法

下面是 OkHttp 的使用示例:

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
                .url(url)
                .build();
// 同步
Response response = client.newCall(request).execute();
// 異步
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

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

首先是創建一個 OkHttpClient 對象,其實用過的應該知道可以用 new OkHttpClient.Builder().build() 的方式來配置 OkHttpClient 的一些參數。有了 OkHttpClient 之后,下面是創建一個 Request 對象,這個對象也是通過 Builder 模式來生成,其中可以配置一些與這條請求相關的參數,其中 url 是必不可少的。在發送請求的時候,需要生成一個 Call 對象,Call 代表了一個即將被執行的請求。如果是同步請求,調用 execute 方法。異步則調用 enqueue,并設定一個回調對象 Callback。

下面就一步步分析發送一條網絡請求的基本流程。

OkHttpClient、Request 及 Call 的創建

OkHttpClient 的創建采用了 Builder 模式,可以配置 Interceptor、Cache 等。可以設置的參數很多,其中部分參數如下:

  final Dispatcher dispatcher;  // 請求的分發器
  final @Nullable Proxy proxy; // 代理
  final List<Protocol> protocols;  // http協議
  final List<ConnectionSpec> connectionSpecs; 
  final List<Interceptor> interceptors;
  final List<Interceptor> networkInterceptors;
  final EventListener.Factory eventListenerFactory;
  final ProxySelector proxySelector;
  final CookieJar cookieJar;
  final @Nullable Cache cache;

Request 與 OkHttpClient 的創建類似,也是用了 Buidler 模式,但是其參數要少很多:

public final class Request {
final HttpUrl url;
final String method;
final Headers headers;
final @Nullable RequestBody body;
final Object tag;
...
}

參數的含義都很明確,即使 Http 協議的url、header、method 以及 body 部分。變量 tag 用于標識一條 Request,可用于發送后取消這條請求。

client.newCall(request) 生成一個 Call 對象。Call 實際上是一個接口,它封裝了 Request,并且用于發起實際的網絡請求。下面是 Call 的全部代碼:

public interface Call extends Cloneable {

  Request request();

  Response execute() throws IOException;

  void enqueue(Callback responseCallback);

  void cancel();

  boolean isExecuted();

  boolean isCanceled();

  Call clone();

  interface Factory {
    Call newCall(Request request);
  }
}

其中包含了與網絡請求相關的操作,包括發起、取消等??匆幌?OkHttpClient 是如何創建 Call 的:

  @Override public Call newCall(Request request) {
    return new RealCall(this, request, false /* for web socket */);
  }

從代碼可以看到,實際上是創建了一個 RealCall 對象,它也是 Call 的唯一一個實現類。

有了 RealCall 對象后,就可以發起網絡請求了,可以是同步請求(execute)或者是異步請求(enqueue)。異步請求涉及到 Dispatcher,先從相對簡單的同步請求開始分析。

同步請求

調用 RealCall#execute() 即是發起同步請求,代碼如下:

  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

首先判斷這條請求是不是已經執行過,如果是則會拋出異常(一條請求只能執行一次,重復執行可以調用 Call#clone())。接著執行了 client.dispatcher().executed(this),這行代碼是把當前的 Call 加入到 Dispatcher 的一個隊列中,這個暫時可以忽略,后面會分析 Dispatcher。

下面一行 Response result = getResponseWithInterceptorChain() 是關鍵,在 getResponseWithInterceptorChain 中真正執行了網絡請求并獲得 Response 并返回。(下一小節具體分析其中的邏輯)

最后在 finally 中調用 Dispatcher 的 finished,從隊列中移除這條請求。

攔截器 Interceptor

getResponseWithInterceptorChain 的代碼如下:

  Response getResponseWithInterceptorChain() throws IOException {
   // Build a full stack of interceptors.
   List<Interceptor> interceptors = new ArrayList<>();
   interceptors.addAll(client.interceptors());
   interceptors.add(retryAndFollowUpInterceptor);
   interceptors.add(new BridgeInterceptor(client.cookieJar()));
   interceptors.add(new CacheInterceptor(client.internalCache()));
   interceptors.add(new ConnectInterceptor(client));
   if (!forWebSocket) {
     interceptors.addAll(client.networkInterceptors());
   }
   interceptors.add(new CallServerInterceptor(forWebSocket));

   Interceptor.Chain chain = new RealInterceptorChain(
       interceptors, null, null, null, 0, originalRequest);
   return chain.proceed(originalRequest);
 }

可以看到,其中創建了一個 List 用于添加 Interceptor。首先添加的是 client 中的 interceptors,也就是在創建 OkHttpClient 對象時自定義的 interceptors,然后依次添加 retryAndFollowUpInterceptor(重試及重定向)、BridgeInterceptor(請求參數的添加)、CacheInterceptor(緩存)、ConnectInterceptor(開始連接)、用戶自定義的 networkinterceptorsCallServerInterceptor(發送參數并讀取響應)。從這里可以知道,OkHttp 默認添加了好幾個 interceptor 用于完成不同的功能。

在研究各個 interceptor 之前,需要考慮一下如何讓這些攔截器一個接著一個的執行?繼續看上面的代碼,在添加了各種 interceptors 之后,創建了一個 RealInterceptorChain 對象。(它的構造函數需要的參數很多,并且這些參數涉及到連接池、請求數據的發送等。由于這篇文章主要分析 OkHttp 的基本流程,所以暫時略過這部分)RealInterceptorChain 是接口 Chain 的實現類,Chain 的意思,其作用是把各個 Interceptor 串起來依次執行。在獲得了 RealInterceptorChain 之后調用其 proceed 方法,看名字就能知道是讓 Request 請求繼續執行。

下面具體分析 RealInterceptorChain,它有如下的成員變量:

  private final List<Interceptor> interceptors;  // 攔截器
  private final StreamAllocation streamAllocation; // 流管理器
  private final HttpCodec httpCodec; // http流,發送請求數據并讀取響應數據
  private final RealConnection connection;  // scoket的連接
  private final int index; // 當前攔截器的索引
  private final Request request; // 當前的請求
  private int calls; // chain 的 proceed 調用次數的記錄

其中 streamAllocation、httpCodecconnection 都與 socket 連接有關,后續文章再分析??匆幌?proceed 方法:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    // 如果已經有了一個流,確保即將到來的 request 是用它
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    // 如果已經有了一個流,確保這是對 call 唯一的調用
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }


    // Call the next interceptor in the chain.         
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);                           // (1)
    Interceptor interceptor = interceptors.get(index); // (2)
    Response response = interceptor.intercept(next);   // (3)

    // Confirm that the next interceptor made its required call to chain.proceed().                                 
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
  }

剛開始做了一些連接方面的判斷,需要關注的是標了(1)、(2)、(3)的幾行,主要做了以下操作:

  1. 創建新的 RealInterceptorChain,其中 index 加1用于標識當前的攔截器
  2. 通過 index 獲取當前的攔截器
  3. 調用下一個攔截器的 intercept 方法,并把上面生成的新的 RealInterceptorChain 對象 next 傳進去

由之前的 getResponseWithInterceptorChain 方法可以知道,當前 RealInterceptorChain 的 interceptors 的第一個是 RetryAndFollowUpInterceptor,下面是其 intercept 的代碼:

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();

    streamAllocation = new StreamAllocation(
        client.connectionPool(), createAddress(request.url()), callStackTrace);

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response = null;
      boolean releaseConnection = true;
      try {
        // 調用 chain 的 proceed
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        ... // 省略部分代碼,主要是錯誤重試以及重定向
    }
  }

這個 Interceptor 主要用于出錯重試以及重定向的邏輯,其中省略了部分代碼。在這個方法當中要關注的是再次調用了 chainproceed 方法,這里的 chain 是之前新創建的 next 對象。相當于說通過調用 Chain#proceed() 將網絡請求推向下一個攔截器(proceed 中會獲取下一個 Interceptor 并調用其 intercept 方法),并且得到 response 對象,而下一個攔截器也是類似的操作。于是,多個 interceptors 就通過這種方式串起來依次執行,并且前一個 Interceptor 可以得到后一個 Interceptor 執行后的 response 從而進行處理。

通過不同的 Interceptor,OkHttp 實現了不同的功能。各個 Inercept 職責分明又不會互相耦合,并且可以非常方便的添加 Interceptor,這是 責任鏈 模式的體現,非常優雅的設計?,F在可以發現 OkHttp 中的攔截器的調用過程如下圖所示:

攔截器調用鏈
攔截器調用鏈

異步請求

相比于同步請求,異步請求主要是增加了 Dispatcher 的處理。Dispatcher 是請求的分發器,它有一下的成員變量:

private int maxRequests = 64;   // 最大連接數
private int maxRequestsPerHost = 5;  // 單個 host 最大連接數
private @Nullable Runnable idleCallback;   // 空閑時的回調

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

/** Ready async calls in the order they'll be run. */
// 準備執行的異步 Call 的隊列
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 中,默認支持的最大并發連接數是64,每個 host 最多可以有5個并發請求。

下面看一下線程池 executorService 的創建。線程池會在兩個地方創建,分別是 Dispatcher 的構造函數或者是 executorService 方法中(如果調用了默認的構造函數):

// 默認構造函數沒有創建
public Dispatcher() {
}
// 自定義線程池
public Dispatcher(ExecutorService executorService) {
    this.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;
  }

Dispatcher 支持自定義的線程池,否則會默認創建一個。在生成 OkHttpClient 對象時,默認調用的是 Dispatcher 無參的構造方法。這個默認線程池通過 new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)) 創建,看上去類似于一個 CachedThreadPool,沒有常駐的 core 線程,空閑線程60秒后自動關閉。

enqueue

每個 Call 被添加到某一個隊列,如果是同步請求添加到 runningSyncCalls 中:

synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
}

異步請求添加的邏輯如下:

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

具體步驟是:

  1. 判斷是否超出總共的最大連接數以及單個 host 的最大連接數
  2. 如果沒有則添加到 runningAsyncCalls 并且提交到線程池執行
  3. 否則添加到 readyAsyncCalls 等待后續執行

需要注意的是異步請求的 Call 不是原始的 Call,而是被包裝為 AsyncCall

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    AsyncCall(Callback responseCallback) {
      super("OkHttp %s", redactedUrl());
      this.responseCallback = responseCallback;
    }
    ...
    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        // 調用 getResponseWithInterceptorChain
        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 {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

AsyncCall 繼承自 NamedRunnable,它其實就是一個為線程設置了名字的 Runnable,在其 Run 中調用 execute,所以 AsyncCall 的主要邏輯都寫在 execute 中??梢钥吹阶罱K還是調用了 getResponseWithInterceptorChain 方法,所以后續執行網絡請求的邏輯是一樣的。在獲得 response 之后,就可以調用 responseCallback 返回最終的信息。

finished

在上面的代碼中,finally 里面執行了 client.dispatcher().finished(this),在同步請求 RealCall#execute() 中也有類似的一行代碼。finished 的作用是讓 Dispatcher 從隊列中移除已完成的 Call,對于異步請求還會從 readyAsyncCalls 中取出等待中的請求提交給線程池。下面是具體代碼:

  /** 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!");
      // 異步請求會進入
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }
    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();
      // 找到一個等待隊列中的 Call,符合連接數要求時加入 runningAsyncCalls 并提交給線程池執行。
      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

有兩個重載的 finished 方法均調用了另一個 pirvate 的 finished,區別在于這個 finished 的最后一個參數 promoteCalls。對于同步請求(參數為 RealCall) promoteCallsfalse,而異步請求(參數為 AsyncCall) promoteCallstrue。 pirvate 的 finished 主要是從隊列中移除 Call,異步請求會執行 promoteCalls。promoteCalls 里面主要是從 readyAsyncCalls 取出一個 Call,如果滿足最大連接數的要求,則把這個 Call 加入 runningAsyncCalls 并提交給線程池執行。

通過 runningAsyncCallsreadyAsyncCalls,Dispatcher 實現了異步請求的調度執行。這里比較巧妙的方式是在 finally 中去執行 readyAsyncCalls 中的請求,避免了 wait/notity 的方式,避免了代碼的復雜性。

總結

OkHttp 的基本執行流程如下圖所示:

OKHttp 基本流程
OKHttp 基本流程

主要是以下步驟:

  1. OkHttpClient 調用 newCall 創建 RealCall 對象,Call 封裝了 Request,代表一條即將執行的請求。
  2. 根據同步還是異步請求分別調用 RealCallexecuteenqueue 方法,將Call 加入 Dispatcher 的相應隊列中。最終,同步或異步請求都會調用 getResponseWithInterceptorChain。
  3. getResponseWithInterceptorChain 中,OkHttp 添加用戶自定義以及默認的 inceptors,并用一個 Chain 管理并依次執行每個 Interceptor。
  4. 每個 Interceptor 調用 Chain#proceed() 將請求發送給下一級的 Inceptor,并能通過這個方法獲得下一級 Interceptor 的 Response。所以上圖所示,Request 一級級地往下傳遞,而獲取了網絡的 Response 之后一級級地往上傳遞。

OkHttp中一條網絡請求的基本流程就是這樣,下一篇文章介紹 OkHttp 如何建立連接:OkHttp 源碼解析(二):建立連接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容