OkHttp解析

Android_FLING發表於2019-04-30

一、整體思路

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

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

image
okhttp_full_process

二、基本用例

來自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

那我們現在就來看看它是如何建立 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);
}

複製程式碼

OkHttp 開發者之一介紹 OkHttp 的文章裡面,作者講到:

the whole thing is just a stack of built-in interceptors.

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

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

image
okhttp_interceptors

  1. 在配置OkHttpClient時設定的interceptors
  2. 負責失敗重試以及重定向的RetryAndFollowUpInterceptor
  3. 負責把使用者構造的請求轉換為傳送到伺服器的請求、把伺服器返回的響應轉換為使用者友好的響應的BridgeInterceptor
  4. 負責讀取快取直接返回、更新快取的CacheInterceptor
  5. 負責和伺服器建立連線的ConnectInterceptor
  6. 配置OkHttpClient時設定的networkInterceptors
  7. 負責向伺服器傳送請求資料、從伺服器讀取響應資料CallServerInterceptor

在這裡,位置決定了功能,最後一個 Interceptor 一定是負責和伺服器實際通訊的,重定向、快取等一定是在實際通訊之前的。

責任鏈模式在這個Interceptor鏈條中得到了很好的實踐。

它包含了一些命令物件和一系列的處理物件,每一個處理物件決定它能處理哪些命令物件,它也知道如何將它不能處理的命令物件傳遞給該鏈中的下一個處理物件。該模式還描述了往該處理鏈的末尾新增新的處理物件的方法。

對於把Request變成Response這件事來說,每個Interceptor都可能完成這件事,所以我們循著鏈條讓每個Interceptor自行決定能否完成任務以及怎麼完成任務(自力更生或者交給下一個Interceptor)。這樣一來,完成網路請求這件事就徹底從RealCall類中剝離了出來,簡化了各自的責任和邏輯。兩個字:優雅!

責任鏈模式在安卓系統中也有比較典型的實踐,例如 view 系統對點選事件(TouchEvent)的處理。

回到 OkHttp,在這裡我們先簡單分析一下ConnectInterceptorCallServerInterceptor,看看 OkHttp 是怎麼進行和伺服器的實際通訊的。

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 協議操作的抽象,有兩個實現:Http1CodecHttp2Codec,顧名思義,它們分別對應 HTTP/1.1 和 HTTP/2 版本的實現。

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

而建立HttpCodec物件的過程涉及到StreamAllocationRealConnection,程式碼較長,這裡就不展開,這個過程概括來說,就是找到一個可用的RealConnection,再利用RealConnection的輸入輸出(BufferedSourceBufferedSink)建立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,並開始執行。

這裡的AsyncCallRealCall的一個內部類,它實現了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時進行設定,這樣就可以使用我們自定義的快取策略了。

三、總結

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

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

image
okhttp_full_process

  • OkHttpClient實現Call.Factory,負責為Request建立Call
  • RealCall為具體的Call實現,其enqueue()非同步介面通過Dispatcher利用ExecutorService實現,而最終進行網路請求時和同步execute()介面一致,都是通過getResponseWithInterceptorChain()函式實現;
  • getResponseWithInterceptorChain()中利用Interceptor鏈條,分層實現快取、透明壓縮、網路 IO 等功能;

相關文章