OkHttp原始碼深度解析

OPPO網際網路技術發表於2020-03-25

本文來自OPPO網際網路基礎技術團隊,轉載請註名作者。同時歡迎關注我們的公眾號:OPPO_tech,與你分享OPPO前沿網際網路技術及活動。

OkHttp應該是目前Android平臺上使用最為廣泛的開源網路庫了,Android 在6.0之後也將內部的HttpUrlConnection的預設實現替換成了OkHttp。

大部分開發的同學可能都有接觸過OkHttp的原始碼,但很少有比較全面的閱讀和了解的,目前網路上的大部分原始碼解析文章也都是點到為止,並且大段的貼原始碼,這種解析方式是我無法認可的,因此才有了想要重新寫一篇解析OkHttp原始碼的想法。

這篇文章的目的,一個是要比較全面的介紹OkHttp原始碼,另一個是要儘量避免大段的貼出原始碼,涉及到原始碼的部分,會盡量通過呼叫關係圖來展示。

本篇選用的OkHttp原始碼是目前最新的4.4.0版本,面向的讀者也是有一定使用基礎的Android開發同學。

OkHttp的原始碼可以從github上下載到(github.com/square/OkHt…

直奔主題,文章將從一下幾個方面開始來拆解OkHttp的原始碼:

  1. 整體結構
  2. 攔截器
  3. 任務佇列
  4. 連線複用和連線池

1. 從一個例子出發

首先來看一個最簡單的Http請求是如何傳送的。

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder().url("www.google.com").build();

Response response = client.newCall(request).execute();

return response.body().string();

這一段程式碼就是日常使用OkHttp最常見的用法,跟進原始碼後,可以得到一張更為詳細的流程圖,通過這張圖來看下內部的邏輯是如何流動的。

OkHttp原始碼深度解析

涉及到了幾個核心類,我們一個個來看下。

  1. OkHttpClient
  2. Request 和 Response
  3. RealCall

OkHttpClient:這個是整個OkHttp的核心管理類,所有的內部邏輯和物件歸OkHttpClient統一來管理,它通過Builder構造器生成,構造引數和類成員很多,這裡先不做具體的分析。

Request 和Response:Request是我們傳送請求封裝類,內部有url, header , method,body等常見的引數,Response是請求的結果,包含code, message, header,body ;這兩個類的定義是完全符合Http協議所定義的請求內容和響應內容。

RealCall :負責請求的排程(同步的話走當前執行緒傳送請求,非同步的話則使用OkHttp內部的執行緒池進行);同時負責構造內部邏輯責任鏈,並執行責任鏈相關的邏輯,直到獲取結果。雖然OkHttpClient是整個OkHttp的核心管理類,但是真正發出請求並且組織邏輯的是RealCall類,它同時肩負了排程和責任鏈組織的兩大重任,接下來我們來著重分析下RealCall類的邏輯。

RealCal類的原始碼地址:github.com/square/OkHt…

RealCall類並不複雜,有兩個最重要的方法,execute() 和 enqueue(),一個是處理同步請求,一個是處理非同步請求。跟進enqueue的原始碼後發現,它只是通過非同步執行緒和callback做了一個非同步呼叫的封裝,最終邏輯還是會呼叫到execute()這個方法,然後呼叫了getResponseWithInterceptorChain()獲得請求結果。

看來是 getResponseWithInterceptorChain() 方法承載了整個請求的核心邏輯,那麼只需要把這個方法分析清楚了,真個OkHttp的請求流程就大體搞明白了。既然這麼重要的方法,還是不能免俗的貼下完整的原始碼。

val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this,
        client.connectTimeoutMillis, client.readTimeoutMillis, client.writeTimeoutMillis)

    var calledNoMoreExchanges = false
    try {
      val response = chain.proceed(originalRequest)
     ............
複製程式碼

從原始碼可以看到,即使是 getResponseWithInterceptorChain() 方法的邏輯其實也很簡單,它生成了一個Interceptors攔截器的List列表,按順序依次將:

  • client.Interceptors
  • RetryAndFollowUpInterceptor,
  • BridgeInterceptor
  • CacheInterceptor
  • ConnectInterceptor
  • client.networkInterceptors
  • CallServerInterceptor

這些成員新增到這個List中,然後建立了一個叫RealInterceptorChain的類,最後的Response就是通過chain.proceed獲取到的。

通過進一步分析RealInterceptorChain和Interceptors,我們得到了一個結論,OkHttp將整個請求的複雜邏輯切成了一個一個的獨立模組並命名為攔截器(Interceptor),通過責任鏈的設計模式串聯到了一起,最終完成請求獲取響應結果。

具體這些攔截器是如何串聯,每個攔截器都有什麼功能,下面的內容會作更詳細的分析。

2. OkHttp的核心:攔截器

我們已經知道了OkHttp的核心邏輯就是一堆攔截器,那麼它們是如何構造並關聯到一起的呢?這裡就要來分析RealInterceptorChain這個類了。

通過前面的分析可知,RealCall將Interceptors一個一個新增到List之後 ,就構造生成了一個RealInterceptorChain物件,並呼叫chain.proceed獲得響應結果。那麼就來分析下chain.proceed這個方法到底幹了啥。為了不讓篇幅太長,這裡就不貼出原始碼內容,僅給出分析後的結論,大家對照原始碼可以很快看懂。

RealInterceptorChain的原始碼:github.com/square/OkHt…

根據對RealInterceptorChain的原始碼解析,可得到如下示意圖(省略了部分攔截器):

OkHttp原始碼深度解析

OkHttp原始碼深度解析

結合原始碼和該示意圖,可以得到如下結論:

  1. 攔截器按照新增順序依次執行
  2. 攔截器的執行從RealInterceptorChain.proceed()開始,進入到第一個攔截器的執行邏輯
  3. 每個攔截器在執行之前,會將剩餘尚未執行的攔截器組成新的RealInterceptorChain
  4. 攔截器的邏輯被新的責任鏈呼叫next.proceed()切分為start、next.proceed、end這三個部分依次執行
  5. next.proceed() 所代表的其實就是剩餘所有攔截器的執行邏輯
  6. 所有攔截器最終形成一個層層內嵌的巢狀結構

瞭解了上面攔截器的構造過程,我們再來一個個的分析每個攔截器的功能和作用。

從這張區域性圖來看,總共新增了五個攔截器(不包含自定義的攔截器如client.interceptors和client.networkInterceptors,這兩個後面再解釋)。

先來大概的瞭解下每一個攔截器的作用

  • retryAndFollowUpInterceptor——失敗和重定向攔截器
  • BridgeInterceptor——封裝request和response攔截器
  • CacheInterceptor——快取相關的過濾器,負責讀取快取直接返回、更新快取
  • ConnectInterceptor——連線服務,負責和伺服器建立連線 這裡才是真正的請求網路
  • CallServerInterceptor——執行流操作(寫出請求體、獲得響應資料) 負責向伺服器傳送請求資料、從伺服器讀取響應資料 進行http請求報文的封裝與請求報文的解析

下面就來一個個的攔截器進行分析。

2.1 RetryAndFollowUpInterceptor

原始碼地址:github.com/square/OkHt…

根據原始碼的邏輯走向,直接畫出對應的流程圖(這段邏輯在RetryAndFollowUpInterceptor的intercept()方法內部):

OkHttp原始碼深度解析

從上圖中可以看出,RetryAndFollowUpInterceptor開啟了一個while(true)的迴圈,並在迴圈內部完成兩個重要的判定,如圖中的藍色方框:

  1. 當請求內部丟擲異常時,判定是否需要重試
  2. 當響應結果是3xx重定向時,構建新的請求併傳送請求

重試的邏輯相對複雜,有如下的判定邏輯(具體程式碼在RetryAndFollowUpInterceptor類的recover方法):

  • 規則1: client的retryOnConnectionFailure引數設定為false,不進行重試
  • 規則2: 請求的body已經發出,不進行重試
  • 規則3: 特殊的異常型別不進行重試(如ProtocolException,SSLHandshakeException等)
  • 規則4: 沒有更多的route(包含proxy和inetaddress),不進行重試

前面這四條規則都不符合的條件下,則會重試當前請求。重定向的邏輯則相對簡單,這裡就不深入了。

2.2 Interceptors和NetworkInterceptors的區別

前面提到,在OkHttpClient.Builder的構造方法有兩個引數,使用者可以通過addInterceptor 和 addNetworkdInterceptor 新增自定義的攔截器,分析完 RetryAndFollowUpInterceptor 我們就可以知道這兩種自動攔截器的區別了。

從前面新增攔截器的順序可以知道 Interceptors 和 networkInterceptors 剛好一個在 RetryAndFollowUpInterceptor 的前面,一個在後面。

結合前面的責任鏈呼叫圖可以分析出來,假如一個請求在 RetryAndFollowUpInterceptor 這個攔截器內部重試或者重定向了 N 次,那麼其內部巢狀的所有攔截器也會被呼叫N次,同樣 networkInterceptors 自定義的攔截器也會被呼叫 N 次。而相對的 Interceptors 則一個請求只會呼叫一次,所以在OkHttp的內部也將其稱之為 Application Interceptor。

2.3 BridgeInterceptor 和 CacheInterceptor

BridageInterceptor 攔截器的功能如下:

  1. 負責把使用者構造的請求轉換為傳送到伺服器的請求 、把伺服器返回的響應轉換為使用者友好的響應,是從應用程式程式碼到網路程式碼的橋樑
  2. 設定內容長度,內容編碼
  3. 設定gzip壓縮,並在接收到內容後進行解壓。省去了應用層處理資料解壓的麻煩
  4. 新增cookie
  5. 設定其他報頭,如User-Agent,Host,Keep-alive等。其中Keep-Alive是實現連線複用的必要步驟

CacheInterceptor 攔截器的邏輯流程如下:

  1. 通過Request嘗試到Cache中拿快取,當然前提是OkHttpClient中配置了快取,預設是不支援的。

  2. 根據response,time,request建立一個快取策略,用於判斷怎樣使用快取。

  3. 如果快取策略中設定禁止使用網路,並且快取又為空,則構建一個Response直接返回,注意返回碼=504

  4. 快取策略中設定不使用網路,但是又快取,直接返回快取

  5. 接著走後續過濾器的流程,chain.proceed(networkRequest)

  6. 當快取存在的時候,如果網路返回的Resposne為304,則使用快取的Resposne。

  7. 構建網路請求的Resposne

  8. 當在OkHttpClient中配置了快取,則將這個Resposne快取起來。

  9. 快取起來的步驟也是先快取header,再快取body。

  10. 返回Resposne

接下來的兩個應該是所有內部攔截器裡最重要的兩個了,一個負責處理Dns和Socket連線,另一個則負責Http請求體的傳送。

2.4 ConnectInterceptor

上面已經提到了,connectInterceptor應該是最重要的攔截器之一了,它同時負責了Dns解析和Socket連線(包括tls連線)。

原始碼地址:github.com/square/OkHt…

這個類本身很簡單,從原始碼來看,關鍵的程式碼只有一句。

val exchange = transmitter.newExchange(chain, doExtensiveHealthChecks)
複製程式碼

從Transmitter獲得了一個新的ExChange的物件,這句簡單的程式碼仔細跟進去以後,會發現其實埋藏了非常多的邏輯,涉及整個網路連線建立的過程,其中包括dns過程和socket連線的過程,這裡我們通過兩個圖來了解下整個網路連線的過程。

OkHttp原始碼深度解析

先來看方法呼叫的時序圖,梳理出關鍵步驟:

  1. ConnectInterceptor呼叫transmitter.newExchange
  2. Transmitter先呼叫ExchangeFinder的find()獲得ExchangeCodec
  3. ExchangeFinder呼叫自身的findHealthConnectio獲得RealConnection
  4. ExchangeFinder通過剛才獲取的RealConnection的codec()方法獲得ExchangeCodec
  5. Transmitter獲取到了ExchangeCodec,然後new了一個ExChange,將剛才的ExchangeCodec包含在內

通過剛才的5步,最終Connectinterceptor通過Transmitter獲取到了一個Exchange的類,這個類有兩個實現,一個是Http1ExchangeCodec,一個是Http2Exchangecodec,分別對應的是Http1協議和Http2協議。

那麼獲取到Exchange類有什麼用呢?再來看這幾個類的關係圖,如下:

OkHttp原始碼深度解析

從上面可以看到,前面獲得的Exchange類裡面包含了ExchangeCodec物件,而這個物件裡面又包含了一個RealConnection物件,RealConnection的屬性成員有socket、handlShake、protocol等,可見它應該是一個Socket連線的包裝類,而ExchangeCode物件是對RealConnection操作(writeRequestHeader、readResposneHeader)的封裝。

通過這兩個圖可以很清晰的知道,最終獲得的是一個已經建立連線的Socket物件,也就是說,在ConnectInterceptor內部已經完成了socket連線,那麼具體是哪一步完成的呢?

看上面的時序圖,可以知道,獲得RealConnection的ExchangeFinder呼叫的findHealthConnection()方法,因此,socket連線的獲取和建立都是在這裡完成的。

同樣,在socket進行連線之前,其實還有一個dns的過程,也是隱含在findHealthConnection 裡的內部邏輯,詳細的過程在後面DNS的過程再進行分析,這裡ConnectionInterceptor的任務已經完成了。

另外還需要注意的一點是,在執行完ConnectInterceptor之後,其實新增了自定義的網路攔截器networkInterceptors,按照順序執行的規定,所有的networkInterceptor執行執行,socket連線其實已經建立了,可以通過realChain拿到socket做一些事情了,這也就是為什麼稱之為network Interceptor的原因。

2.5 CallServerInterceptor

CalllServerInterceptor是最後一個攔截器了,前面的攔截器已經完成了socket連線和tls連線,那麼這一步就是傳輸http的頭部和body資料了。

CallServerInterceptor原始碼:github.com/square/OkHt…

CallServerInterceptor由以下步驟組成:

  1. 向伺服器傳送 request header
  2. 如果有 request body,就向伺服器傳送
  3. 讀取 response header,先構造一個 Response 物件
  4. 如果有 response body,就在 3 的基礎上加上 body 構造一個新的 Response 物件

這裡我們可以看到,核心工作都由 HttpCodec 物件完成,而 HttpCodec 實際上利用的是 Okio,而 Okio 實際上還是用的 Socket,只不過一層套一層,層數有點多。

3. 整體架構

至此為止,所有的攔截器都講完了,我們已經知道了一個完整的請求流程是如何發生的。那麼這個時候再來看OkHttp的架構圖就比較清晰了

OkHttp原始碼深度解析

整個OkHttp的架構縱向來看就是五個內部攔截器,橫向來看被切分成了幾個部分,而縱向的攔截器就是通過對橫向分層的呼叫來完成整個請求過程,從這兩個方面來把握和理解OkHttp就比較全面了。

針對橫向的部分將在接下來的部分進行詳細分析。

3.1 連線複用,DNS和Socket的連線

通過前面的分析知道,Socket連線和Dns過程都是在ConnecInterceptor中通過Transmitter和ExchangeFinder來完成的,而在前面的時序圖中可以看到,最終建立Socket連線的方法是通過ExchangeFinder的findConnection來完成的,可以說一切祕密都是findConnection方法中。

因此接下來詳細解析下findConnection(),這裡貼出原始碼和註釋。

synchronized(connectionPool) {
      //前面有一大段判定當前的conencection是否需要釋放,先刪除
      .....

      if (result == null) {
        // 1, 第一次嘗試從緩衝池裡面獲取RealConnection(Socket的包裝類)
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true
          result = transmitter.connection
        } else if (nextRouteToTry != null) {
          //2, 如果緩衝池中沒有,則看看有沒有下一個Route可以嘗試,這裡只有重試的情況會走進來
          selectedRoute = nextRouteToTry
          nextRouteToTry = null
        } else if (retryCurrentRoute()) {
          //3,如果已經設定了使用當前Route重試,那麼會繼續使用當前的Route
          selectedRoute = transmitter.connection!!.route()
        }
      }
    }
    if (result != null) {
      // 4,如果前面發現ConnectionPool或者transmiter中有可以複用的Connection,這裡就直接返回了
      return result!!
    }

    // 5, 如果前面沒有獲取到Connection,這裡就需要通過routeSelector來獲取到新的Route來進行Connection的建立
    var newRouteSelection = false
    if (selectedRoute == null && (routeSelection == null || !routeSelection!!.hasNext())) {
      newRouteSelection = true
      //6,獲取route的過程其實就是DNS獲取到域名IP的過程,這是一個阻塞的過程,會等待DNS結果返回
      routeSelection = routeSelector.next()
    }

    var routes: List<Route>? = null
    synchronized(connectionPool) {
      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        routes = routeSelection!!.routes
        //7,前面如果通過routeSelector拿到新的Route,其實就是相當於拿到一批新的IP,這裡會再次嘗試從ConnectionPool
        // 中檢查是否有可以複用的Connection
        if (connectionPool.transmitterAcquirePooledConnection( address, transmitter, routes, false)) {
          foundPooledConnection = true
          result = transmitter.connection
        }
      }
 if (!foundPooledConnection) {
        if (selectedRoute == null) {
           //8,前面我們拿到的是一批IP,這裡通過routeSelection獲取到其中一個IP,Route是proxy和InetAddress的包裝類
          selectedRoute = routeSelection!!.next()
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        //9,用新的route建立RealConnection,注意這裡還沒有嘗試連線
        result = RealConnection(connectionPool, selectedRoute!!)
        connectingConnection = result
      }
  }

    // If we found a pooled connection on the 2nd time around, we're done.
    // 10,註釋說得很清楚,如果第二次從connectionPool獲取到Connection可以直接返回了
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result!!)
      return result!!
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    //11,原文註釋的很清楚,這裡是進行TCP + TLS連線的地方
    result!!.connect(
        connectTimeout,   
 readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
    )
 //後面一段是將連線成功的RealConnection放到ConnectionPool裡面,這裡就不貼出來了
    }
    return result!!
複製程式碼

從上面的流程可以看到,findConnection這個方法做了以下幾件事:

  1. 檢查當前exchangeFinder所儲存的Connection是否滿足此次請求
  2. 檢查當前連線池ConnectionPool中是否滿足此次請求的Connection
  3. 檢查當前RouteSelector列表中,是否還有可用Route(Route是proxy,IP地址的包裝類),如果沒有就發起DNS請求
  4. 通過DNS獲取到新的Route之後,第二次從ConnectionPool查詢有無可複用的Connection,否則就建立新的RealConnection
  5. 用RealConnection進行TCP和TLS連線,連線成功後儲存到ConnectionPool

Connection的連線複用

可以看到,第二步和第四步對ConnectionPool做了兩次複用檢查,第五步建立了新的RealConnection之後就會寫會到ConnectionPool中。

因此這裡就是OkHttp的連線複用其實是通過ConnectionPool來實現的,前面的類圖中也反映出來,ConnectionPool內部有一個connections的ArrayDeque物件就是用來儲存快取的連線池。

DNS過程

從前面解析的步驟可知,Dns的過程隱藏在了第三步RouteSelector檢查中,整個過程在findConnection方法中寫的比較散,可能不是特別好理解,但是隻要搞明白了RouteSelector, RouteSelection,Route這三個類的關係,其實就比較容易理解了,如下圖中展示了三個類之間的關係。

OkHttp原始碼深度解析

從圖中可以得到如下結論:

  • RouteSelector在呼叫next遍歷在不同proxy情況下獲得下一個Selection封裝類,Selection持有一個Route的列表,也就是每個proxy都對應有Route列表
  • Selection其實就是針對List封裝的一個迭代器,通過next()方法獲得下一個Route,Route持有proxy、address和inetAddress,可以理解為Route就是針對IP和Proxy配對的一個封裝
  • RouteSelector的next()方法內部呼叫了nextProxy(), nextProxy()又會呼叫resetNextInetSocketAddres()方法
  • resetNextInetSocketAddres通過address.dns.lookup獲取InetSocketAddress,也就是IP地址

通過上面一系列流程知道,IP地址最終是通過address的dns獲取到的,而這個dns又是怎麼構建的呢?

反向追蹤程式碼,定位到address的dns是transmitter在構建address的時候,將內建的client.dns傳遞進來,而client.dns是在OkHttpclient的構建過程中傳遞進來Dns.System,裡面的lookup是通過InetAddress.getAllByName 方法獲取到對應域名的IP,也就是預設的Dns實現。

至此,整個DNS的過程就真相大白了。OkHttp在這一塊設計的時候,為了強調接耦和開放性,將DNS的整個過程隱藏的比較深,如果不仔細debug跟程式碼的話,可能還不是很容易發現。

3.2 Socket連線的建立

通過Dns獲得Connectoin之後,就是建立連線的過程了,在findConnection中只體現為一句程式碼,如下:

result!!.connect(
        connectTimeout,
        readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
)
複製程式碼

這裡的result是RealConnection型別的物件,就是呼叫了RealConnection.connect方法,終於離開findConnection 了,接下來看下connect 方法的原始碼。

//省略前面一大段 
....

    while (true) {
      try {
        if (route.requiresTunnel()) {
 //這裡進入的條件是,通過http代理了https請求,有一個特殊的協議交換過程
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
        } else {
 //建立socket連線
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
 //如果前面判定是https請求,這裡就是https的tls建立過程
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        break
      } catch (e: IOException) {
 //清理資源
        socket?.closeQuietly()
 //對異常做二次封裝,然後丟擲
        if (routeException == null) {
          routeException = RouteException(e)
        } else {
          routeException.addConnectException(e)
        }
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException
        }
      }
    }
複製程式碼

connect方法並不複雜,先會判定是否有代理的情況做一些特殊處理,然後呼叫系統方法建立socket連線。

如果是https請求,還有一個tls的連線要建立,這中間如果有丟擲異常,會做一個二次封裝再丟擲去。

4. 總結

到此為止,基本上OkHttp原始碼設計的一個全貌都有了,有一些內容因為日常使用經常會遇到,比如OkHttpClient的Builde引數,比如Request和Response的用法,這裡就不再多講。另外針對http2和https的支援,因為涉及到更為複雜的機制和原理,以後有機會另開一篇文章來說。

OkHttp原始碼深度解析

相關文章