拆輪子系列:拆 OkHttp

原文地址:http://blog.piasy.com/2016/07/11/Understand-OkHttp/

安卓開發領域,很多重要的問題都有了很好的開源解決方案,例如網絡請求 OkHttp + Retrofit 簡直就是不二之選。“我們不重複造輪子不表示我們不需要知道輪子該怎麼造及如何更好的造!”,在用了這些好輪子將近兩年之後,現在是時候拆開輪子一探究竟了。本文基於 OkHttp 截至 2016.7.11 的最新源碼對其進行了詳細分析。

1,整體思路

從使用方法出發,首先是怎麼使用,其次是我們使用的功能在內部是如何實現的,實現方案上有什麼技巧,有什麼範式。全文基本上是對 OkHttp 源碼的一個分析與導讀,非常建議大家下載 OkHttp 源碼之後,跟着本文,過一遍源碼。對於技巧和範式,由於目前我的功力還不到位,分析內容沒多少,歡迎大家和我一起討論。

首先放一張完整流程圖(看不懂沒關係,慢慢往後看):

okhttp_full_process

2,基本用例

來自 OkHttp 官方網站

2.1,創建 OkHttpClient 對象

OkHttpClient client = new OkHttpClient();

咦,怎麼不見 builder?莫急,且看其構造函數:

public OkHttpClient() {
  this(new Builder());
}

原來是方便我們使用,提供了一個“快捷操作”,全部使用了默認的配置。OkHttpClient.Builder 類成員很多,後面我們再慢慢分析,這裏先暫時略過:

public Builder() {
  dispatcher = new Dispatcher();
  protocols = DEFAULT_PROTOCOLS;
  connectionSpecs = DEFAULT_CONNECTION_SPECS;
  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;
}

2.2,發起 HTTP 請求

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

OkHttpClient 實現了 Call.Factory,負責根據請求創建新的 Call,在 拆輪子系列:拆 Retrofit中我們曾和它發生過一次短暫的遭遇:

callFactory 負責創建 HTTP 請求,HTTP 請求被抽象爲了 okhttp3.Call 類,它表示一個已經準備好,可以隨時執行的 HTTP 請求

那我們現在就來看看它是如何創建 Call 的:

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

如此看來功勞全在 RealCall 類了,下面我們一邊分析同步網絡請求的過程,一邊瞭解 RealCall 的具體內容。

2.2.1,同步網絡請求

我們首先看 RealCall#execute

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

這裏我們做了 4 件事:

  1. 檢查這個 call 是否已經被執行了,每個 call 只能被執行一次,如果想要一個完全一樣的 call,可以利用 call#clone 方法進行克隆。
  2. 利用 client.dispatcher().executed(this) 來進行實際執行,dispatcher 是剛纔看到的OkHttpClient.Builder 的成員之一,它的文檔說自己是異步 HTTP 請求的執行策略,現在看來,同步請求它也有摻和。
  3. 調用 getResponseWithInterceptorChain() 函數獲取 HTTP 返回結果,從函數名可以看出,這一步還會進行一系列“攔截”操作。
  4. 最後還要通知 dispatcher 自己已經執行完畢。

dispatcher 這裏我們不過度關注,在同步執行的流程中,涉及到 dispatcher 的內容只不過是告知它我們的執行狀態,比如開始執行了(調用 executed),比如執行完畢了(調用 finished),在異步執行流程中它會有更多的參與。

真正發出網絡請求,解析返回結果的,還是 getResponseWithInterceptorChain

private 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 (!retryAndFollowUpInterceptor.isForWebSocket()) {
    interceptors.addAll(client.networkInterceptors());
  }
  interceptors.add(new CallServerInterceptor(
      retryAndFollowUpInterceptor.isForWebSocket()));

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

Interceptor 是最核心的一個東西,不要誤以爲它只負責攔截請求進行一些額外的處理(例如 cookie),實際上它把實際的網絡請求、緩存、透明壓縮等功能都統一了起來,每一個功能都只是一個 Interceptor,它們再連接成一個 Interceptor.Chain,環環相扣,最終圓滿完成一次網絡請求。

從 getResponseWithInterceptorChain 函數我們可以看到,Interceptor.Chain 的分佈依次是:

okhttp_interceptors

  1. 在配置 OkHttpClient 時設置的 interceptors
  2. 負責失敗重試以及重定向的 RetryAndFollowUpInterceptor
  3. 負責把用戶構造的請求轉換爲發送到服務器的請求、把服務器返回的響應轉換爲用戶友好的響應的 BridgeInterceptor
  4. 負責讀取緩存直接返回、更新緩存的 CacheInterceptor
  5. 負責和服務器建立連接的 ConnectInterceptor
  6. 配置 OkHttpClient 時設置的 networkInterceptors
  7. 負責向服務器發送請求數據、從服務器讀取響應數據的 CallServerInterceptor

在這裏,位置決定了功能,最後一個 Interceptor 一定是負責和服務器實際通訊的,重定向、緩存等一定是在實際通訊之前的。我現在無法準確將這一設計對應到哪個或者哪幾個設計模式,但我不得不感嘆,確實很優雅!

在這裏我們先簡單分析一下 ConnectInterceptor 和 CallServerInterceptor,看看 OkHttp 是怎麼進行和服務器的實際通信的,其他的 interceptor 我們後面再一一分析。

2.2.1.1,建立連接:ConnectInterceptor
@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Request request = realChain.request();
  StreamAllocation streamAllocation = realChain.streamAllocation();

  // We need the network to satisfy this request. Possibly for validating a conditional GET.
  boolean doExtensiveHealthChecks = !request.method().equals("GET");
  HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
  RealConnection connection = streamAllocation.connection();

  return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

實際上建立連接就是創建了一個 HttpCodec 對象,它將在後面的步驟中被使用,那它又是何方神聖呢?它是對 HTTP 協議操作的抽象,有兩個實現:Http1Codec 和 Http2Codec,顧名思義,它們分別對應 HTTP 1.x 和 HTTP 2.x 版本的實現。

在 Http1Codec 中,它利用 Okio 對 Socket 的讀寫操作進行封裝,Okio 以後有機會再進行分析,現在讓我們對它們保持一個簡單地認識:它對 java.io 和 java.nio 進行了封裝,讓我們更便捷高效的進行 IO 操作。

而創建 HttpCodec 對象的過程涉及到 StreamAllocationRealConnection,代碼較長,這裏就不展開,這個過程概括來說,就是找到一個可用的 RealConnection,再利用 RealConnection 的輸入輸出(BufferedSource 和 BufferedSink)創建 HttpCodec 對象,供後續步驟使用。

2.2.1.2,發送和接收數據:CallServerInterceptor
@Override public Response intercept(Chain chain) throws IOException {
  HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
  StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
  Request request = chain.request();

  long sentRequestMillis = System.currentTimeMillis();
  httpCodec.writeRequestHeaders(request);

  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody);
    bufferedRequestBody.close();
  }

  httpCodec.finishRequest();

  Response response = httpCodec.readResponseHeaders()
      .request(request)
      .handshake(streamAllocation.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();

  if (!forWebSocket || response.code() != 101) {
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
  }

  if ("close".equalsIgnoreCase(response.request().header("Connection"))
      || "close".equalsIgnoreCase(response.header("Connection"))) {
    streamAllocation.noNewStreams();
  }

  // 省略部分檢查代碼

  return response;
}

我們抓住主幹部分:

  1. 向服務器發送 request header;
  2. 如果有 request body,就向服務器發送;
  3. 讀取 response header,先構造一個 Response 對象;
  4. 如果有 response body,就在 3 的基礎上加上 body 構造一個新的 Response 對象;

這裏我們可以看到,核心工作都由 HttpCodec 對象完成,而 HttpCodec 實際上利用的是 Okio,而 Okio 實際上還是用的 Socket,所以沒什麼神祕的,只不過一層套一層,層數有點多。

其實 Interceptor 的設計也是一種分層的思想,每個 Interceptor 就是一層。爲什麼要套這麼多層呢?分層的思想在 TCP/IP 協議中就體現得淋漓盡致,分層簡化了每一層的邏輯,每層只需要關注自己的責任(單一原則思想也在此體現),而各層之間通過約定的接口/協議進行合作(面向接口編程思想),共同完成複雜的任務。

簡單應該是我們的終極追求之一,儘管有時爲了達成目標不得不復雜,但如果有另一種更簡單的方式,我想應該沒有人不願意替換。

2.2.2,發起異步網絡請求

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

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        System.out.println(response.body().string());
    }
});

// RealCall#enqueue
@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

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

這裏我們就能看到 dispatcher 在異步執行時發揮的作用了,如果當前還能執行一個併發請求,那就立即執行,否則加入 readyAsyncCalls 隊列,而正在執行的請求執行完畢之後,會調用promoteCalls() 函數,來把 readyAsyncCalls 隊列中的 AsyncCall “提升”爲runningAsyncCalls,並開始執行。

這裏的 AsyncCall 是 RealCall 的一個內部類,它實現了 Runnable,所以可以被提交到ExecutorService 上執行,而它在執行時會調用 getResponseWithInterceptorChain() 函數,並把結果通過 responseCallback 傳遞給上層使用者。

這樣看來,同步請求和異步請求的原理是一樣的,都是在 getResponseWithInterceptorChain() 函數中通過 Interceptor 鏈條來實現的網絡請求邏輯,而異步則是通過 ExecutorService 實現。

2.3,返回數據的獲取

在上述同步(Call#execute() 執行之後)或者異步(Callback#onResponse() 回調中)請求完成之後,我們就可以從 Response 對象中獲取到響應數據了,包括 HTTP status code,status message,response header,response body 等。這裏 body 部分最爲特殊,因爲服務器返回的數據可能非常大,所以必須通過數據流的方式來進行訪問(當然也提供了諸如 string() 和 bytes() 這樣的方法將流內的數據一次性讀取完畢),而響應中其他部分則可以隨意獲取。

響應 body 被封裝到 ResponseBody 類中,該類主要有兩點需要注意:

  1. 每個 body 只能被消費一次,多次消費會拋出異常;
  2. body 必須被關閉,否則會發生資源泄漏;

在 2.2.1.2,發送和接收數據:CallServerInterceptor 小節中,我們就看過了 body 相關的代碼:

if (!forWebSocket || response.code() != 101) {
  response = response.newBuilder()
      .body(httpCodec.openResponseBody(response))
      .build();
}

由 HttpCodec#openResponseBody 提供具體 HTTP 協議版本的響應 body,而 HttpCodec 則是利用 Okio 實現具體的數據 IO 操作。

這裏有一點值得一提,OkHttp 對響應的校驗非常嚴格,HTTP status line 不能有任何雜亂的數據,否則就會拋出異常,在我們公司項目的實踐中,由於服務器的問題,偶爾 status line 會有額外數據,而服務端的問題也毫無頭緒,導致我們不得不忍痛繼續使用 HttpUrlConnection,而後者在一些系統上又存在各種其他的問題,例如魅族系統發送 multi-part form 的時候就會出現沒有響應的問題。

2.4,HTTP 緩存

在 2.2.1,同步網絡請求 小節中,我們已經看到了 Interceptor 的佈局,在建立連接、和服務器通訊之前,就是 CacheInterceptor,在建立連接之前,我們檢查響應是否已經被緩存、緩存是否可用,如果是則直接返回緩存的數據,否則就進行後面的流程,並在返回之前,把網絡的數據寫入緩存。

這塊代碼比較多,但也很直觀,主要涉及 HTTP 協議緩存細節的實現,而具體的緩存邏輯 OkHttp 內置封裝了一個 Cache 類,它利用 DiskLruCache,用磁盤上的有限大小空間進行緩存,按照 LRU 算法進行緩存淘汰,這裏也不再展開。

我們可以在構造 OkHttpClient 時設置 Cache 對象,在其構造函數中我們可以指定目錄和緩存大小:

public Cache(File directory, long maxSize);

而如果我們對 OkHttp 內置的 Cache 類不滿意,我們可以自行實現 InternalCache 接口,在構造OkHttpClient 時進行設置,這樣就可以使用我們自定義的緩存策略了。

3,總結

OkHttp 還有很多細節部分沒有在本文展開,例如 HTTP2/HTTPS 的支持等,但建立一個清晰的概覽非常重要。對整體有了清晰認識之後,細節部分如有需要,再單獨深入將更加容易。

在文章最後我們再來回顧一下完整的流程圖:

okhttp_full_process

  • OkHttpClient 實現 Call.Factory,負責爲 Request 創建 Call
  • RealCall 爲具體的 Call 實現,其 enqueue() 異步接口通過 Dispatcher 利用ExecutorService 實現,而最終進行網絡請求時和同步 execute() 接口一致,都是通過getResponseWithInterceptorChain() 函數實現;
  • getResponseWithInterceptorChain() 中利用 Interceptor 鏈條,分層實現緩存、透明壓縮、網絡 IO 等功能;
發佈了54 篇原創文章 · 獲贊 22 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章