【Android】OkHttp原始碼解讀逐字稿(2)-OkHttpClient

語言: CN / TW / HK

圖片1.png 如期而至,相約的文章,它來咯~

在開始之前,我想說,這兩篇文章都稍微比較長,但是如果真的能認真的看完,理解完,內功會蹭蹭往上漲。同時,面對相應的面試題,也能自如應對。

上一篇的傳送 【Android】OkHttp原始碼解讀逐字稿(1)-攔截器

0.前言

本篇文章,主要探索 OkHttpClient 的一些配置。

先上圖,基於上次的程式碼,我們在 OkHttpClient() 後加上一個 . 我們就可以看到如下圖的多個屬性和方法。那具體他們的作用是什麼呢?那就是我們本篇文章的任務了。走帶著該死的好奇心,一起去瞧瞧。


進入到 OkHttpClient 的程式碼中,在剛開始的程式碼中,列出了若干個屬性,就是我們要研究的。我先粘出來,這樣方便我們一個個分析。

@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher @get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool @get:JvmName("interceptors") val interceptors: List<Interceptor> = builder.interceptors.toImmutableList() @get:JvmName("networkInterceptors") val networkInterceptors: List<Interceptor> = builder.networkInterceptors.toImmutableList() @get:JvmName("eventListenerFactory") val eventListenerFactory: EventListener.Factory = builder.eventListenerFactory @get:JvmName("retryOnConnectionFailure") val retryOnConnectionFailure: Boolean = builder.retryOnConnectionFailure @get:JvmName("authenticator") val authenticator: Authenticator = builder.authenticator @get:JvmName("followRedirects") val followRedirects: Boolean = builder.followRedirects @get:JvmName("followSslRedirects") val followSslRedirects: Boolean = builder.followSslRedirects @get:JvmName("cookieJar") val cookieJar: CookieJar = builder.cookieJar @get:JvmName("cache") val cache: Cache? = builder.cache @get:JvmName("dns") val dns: Dns = builder.dns @get:JvmName("proxy") val proxy: Proxy? = builder.proxy @get:JvmName("proxySelector") val proxySelector: ProxySelector = when { // Defer calls to ProxySelector.getDefault() because it can throw a SecurityException. builder.proxy != null -> NullProxySelector else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector } @get:JvmName("proxyAuthenticator") val proxyAuthenticator: Authenticator = builder.proxyAuthenticator @get:JvmName("socketFactory") val socketFactory: SocketFactory = builder.socketFactory private val sslSocketFactoryOrNull: SSLSocketFactory? @get:JvmName("sslSocketFactory") val sslSocketFactory: SSLSocketFactory get() = sslSocketFactoryOrNull ?: throw IllegalStateException("CLEARTEXT-only client") @get:JvmName("x509TrustManager") val x509TrustManager: X509TrustManager? @get:JvmName("connectionSpecs") val connectionSpecs: List<ConnectionSpec> = builder.connectionSpecs @get:JvmName("protocols") val protocols: List<Protocol> = builder.protocols @get:JvmName("hostnameVerifier") val hostnameVerifier: HostnameVerifier = builder.hostnameVerifier @get:JvmName("certificatePinner") val certificatePinner: CertificatePinner @get:JvmName("certificateChainCleaner") val certificateChainCleaner: CertificateChainCleaner? @get:JvmName("callTimeoutMillis") val callTimeoutMillis: Int = builder.callTimeout @get:JvmName("connectTimeoutMillis") val connectTimeoutMillis: Int = builder.connectTimeout @get:JvmName("readTimeoutMillis") val readTimeoutMillis: Int = builder.readTimeout @get:JvmName("writeTimeoutMillis") val writeTimeoutMillis: Int = builder.writeTimeout @get:JvmName("pingIntervalMillis") val pingIntervalMillis: Int = builder.pingInterval


1.各個屬性淺析

01.dispatcher

如果仔細點的道友,可以發現在上篇文章中的開始分析的第一段程式碼中,就出現了它。當時,我們並沒有分析它,那現在我們再來看看吧。 ``` override fun execute(): Response { check(executed.compareAndSet(false, true)) { "Already Executed" }

timeout.enter() callStart() try { client.dispatcher.executed(this) return getResponseWithInterceptorChain() } finally { client.dispatcher.finished(this) } } ![](<> "點選並拖拽以移動") 跟進! runningSyncCalls.add(call)

/ Running synchronous calls. Includes canceled calls that haven't finished yet. */ private val runningSyncCalls = ArrayDeque() ``` 可以看到上面的註釋,我們知道了就是將當前的 call 放進了一個佇列中**這還是一個同步的 call。

居然有同步,我們很自然就會想到非同步這個概念。

正如我們所料,確實,還有另外兩個佇列。一個是 readyAsyncCalls, runningAsyncCalls。看註釋,我們就知道他們的作用了。

``` /* Ready async calls in the order they'll be run. / private val readyAsyncCalls = ArrayDeque()

/* Running asynchronous calls. Includes canceled calls that haven't finished yet. / private val runningAsyncCalls = ArrayDeque() ``` 這時候,我們應該想起。我們使用 OkHttp 做一個網路請求時,我們還可以使用 enqueue 的方法

client.newCall(request).enqueue(object : Callback{ override fun onFailure(call: Call, e: IOException) { } override fun onResponse(call: Call, response: Response) { } })

同理看到 RealCall 中對應的方法

``` override fun enqueue(responseCallback: Callback) { check(executed.compareAndSet(false, true)) { "Already Executed" }

callStart() client.dispatcher.enqueue(AsyncCall(responseCallback)) } ```

發現,它呼叫了 dispatcher 的同名方法。

``` internal fun enqueue(call: AsyncCall) { synchronized(this) { readyAsyncCalls.add(call)

// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.call.forWebSocket) {
  val existingCall = findExistingCallWithHost(call.host)
  if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}

} promoteAndExecute() } ```

同樣看返回值,看到最後一行

``` /* * Promotes eligible calls from [readyAsyncCalls] to [runningAsyncCalls] and runs them on the * executor service. Must not be called with synchronization because executing calls can call * into user code. * * @return true if the dispatcher is currently running calls. / private fun promoteAndExecute(): Boolean { this.assertThreadDoesntHoldLock()

val executableCalls = mutableListOf() val isRunning: Boolean synchronized(this) { val i = readyAsyncCalls.iterator() while (i.hasNext()) { val asyncCall = i.next()

  if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
  if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

  i.remove()
  asyncCall.callsPerHost.incrementAndGet()
  executableCalls.add(asyncCall)
  runningAsyncCalls.add(asyncCall)
}
isRunning = runningCallsCount() > 0

}

for (i in 0 until executableCalls.size) { val asyncCall = executableCalls[i] asyncCall.executeOn(executorService) } return isRunning } ``` 看到註釋,我們可以得知,這個方法,就是將一個可以被執行的 call 用 executor ervices 跑起來。看上面兩個 if。只要經歷過這兩個 if 的篩選的話,就可以 executableCalls.add(asyncCall) 。

那我們這兩個if ,主要做了什麼。

1.判斷正在執行的 call 的個數是否大於 maxRequests

2.判斷同一個Host下的 call 個數是否大於 maxRequestsPerHost

至於,maxRequests 和 maxRequestsPerHost 就不細講了。見明知意,maxRequests 預設值為 5 ,maxRequestsPerHost 預設值是 64。

我們看看 dispatchtor 這個類的註釋,做一個小結

/** * Policy on when async requests are executed. * * Each dispatcher uses an [ExecutorService] to run calls internally. If you supply your own * executor, it should be able to run [the configured maximum][maxRequests] number of calls * concurrently. */ 總的來說,就是會通過一個 ExecutorService 去執行一個call。

最後,留一個疑問,“enqueue 方法,是通過 promoteAndExcute 方法,去呼叫一個 ExecutorService 去執行。那 execute() 是通過什麼去執行一個 call的呢?”


02.connectionPool

這個跟我們的 ThreadPool ,具有異曲同工之妙

看下這個類的註釋

* Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that * share the same [Address] may share a [Connection]. This class implements the policy * of which connections to keep open for future use.

這裡,單獨說明了,HTTP 當同樣的 IP 地址的時候,才可以共享一個連線。

那麼 HTTP2 是具有多路複用(Multipex)的能力的。


03.interceptors & networkInterceptors

這個就是將我們自定義的攔截器接收下來,然後在上篇文章中的

getResponseWithInterceptorChain() 加入到攔截器鏈條中


04.eventListenerFactory

事件監聽器工廠類,

EventListener

* Listener for metrics events. Extend this class to monitor the quantity, size, and duration of * your application's HTTP calls.


05.retryOnConnectionFailure

這個返回的是個布林值,所以這是個開關。

看名字,我們可以知道,“是否要在失敗的時候,進行重試”


06.authenticator

自動認證修訂的工具。

tokon 是一個有效期,這時候需要通過 refreshToken 去重新獲取 token


07.followRedirects & followSslRedirects

同理,返回的是個布林值,所以是個開關。

“再發生重定向的時候,是否要重定向”

“在發生協議重定向(http->https),是否要重定向”


08.cookieJar

CookieJar 類,主要有兩個方法

fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) fun loadForRequest(url: HttpUrl): List<Cookie>

從相應頭中儲存 對應 url 的 cookies 資訊

在請求頭中帶上 對應 url 的 cookies 資訊


09.cache

* Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and * bandwidth.

這個是快取器,主要快取一些資料,在下次獲取的時候,可以直接從cache中獲取,從而可以達到節省頻寬的效果


10.dns

Domain name service 域名解析器。將 host 解析成對應的 IP 地址

核心程式碼

InetAddress.getAllByName(hostname).toList()


11.proxy & proxySelector & proxyAuthenticator

代理,如果需要請求需要通過代理伺服器來負責,則新增一個代理。

這裡需要注意一下它的型別,其中包括 直連,這也是預設方法。也就是不需要代理。

public enum Type { /** * Represents a direct connection, or the absence of a proxy. */ DIRECT, /** * Represents proxy for high level protocols such as HTTP or FTP. */ HTTP, /** * Represents a SOCKS (V4 or V5) proxy. */ SOCKS };

而 proxy 的預設值為 null

internal var proxy: Proxy? = null

那怎麼預設到 直連呢?那就是 proxySelector

when { // Defer calls to ProxySelector.getDefault() because it can throw a SecurityException. builder.proxy != null -> NullProxySelector else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector }

如果 proxy 為空 且 預設的 proxySelector 也是空的話,就預設為 NullProxySelector 。而它最後返回的是 Type.DIRECT 的代理

``` public final static Proxy NO_PROXY = new Proxy();

// Creates the proxy that represents a {@code DIRECT} connection. private Proxy() { type = Type.DIRECT; sa = null; } ```

最後 proxyAuthenticator ,就是用於驗證 代理伺服器 的合法性的。


12.socketFactory & sslSocketFactory

我們進行 HTTP 連線請求本質上是一個 socket。 就是通過 socketFactory 去建立。

同時,我們進行加密連線 SSL 連線的時候,這是的 socket 就是通過 sslSocketFactory 去建立的。


13.x509TrustManager

首先 x509 是一種證書格式。這個類就是驗證證書的合法性的。(如果不太明白,可以自行先了解一下 HTTPS 連線流程其中的安全性是如何保證的?後續,有空有把一些相關基礎性的東西補充成另一篇文章)


14.connectionSpecs

連線標準。

在請求連線的時候,客戶端需要向伺服器,傳送支援的協議,加密套件等資訊 (看不懂,同上)

這裡,我們還需要知道是,提供的四種配置。

``` / A secure TLS connection that requires a recent client platform and a recent server. / @JvmField val RESTRICTED_TLS = Builder(true) .cipherSuites(RESTRICTED_CIPHER_SUITES) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2) .supportsTlsExtensions(true) .build() / * A modern TLS configuration that works on most client platforms and can connect to most servers. * This is OkHttp's default configuration. / @JvmField val MODERN_TLS = Builder(true) .cipherSuites(APPROVED_CIPHER_SUITES) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2) .supportsTlsExtensions(true) .build()

/ * A backwards-compatible fallback configuration that works on obsolete client platforms and can * connect to obsolete servers. When possible, prefer to upgrade your client platform or server * rather than using this configuration. / @JvmField val COMPATIBLE_TLS = Builder(true) .cipherSuites(APPROVED_CIPHER_SUITES) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) .supportsTlsExtensions(true) .build()

/* Unencrypted, unauthenticated connections for http: URLs. / @JvmField val CLEARTEXT = Builder(false).build() ```

其中最後一種 明文傳輸就是 HTTP 。

第一種限制更多;第二種是比較流行的,同時也是預設值;第三種限制比較少,即相容性更好。


15.protocols

支援的協議版本號,例如:HTTP_1_0; HTTP_1_1;HTTP_2;H2_PRIOR_KNOWLEDGE(不加密,明文傳輸的時候使用)


16.hostnameVerifier

在驗證 證書的合法性的同時,我們還需要驗證是這個證書是哪個網站的,那麼就需要這個 hostnameVerifier 來驗證。


17.certificatePinner

這個可以用來對某個網站,在驗證證書的合法性同時,要滿足我們指定的證書雜湊值。但是不建議這麼做,因為在更換驗證機構後,會導致之前的使用者無法正常使用我們的應用。


18.certificateChainCleaner

這是一個 X509TrustManager 的操作員


19.一組跟時間有關的屬性 callTimeoutMillis & connectTimeoutMillis & readTimeoutMillis & writeTimeoutMillis & pingIntervalMillis

``` /* * Default call timeout (in milliseconds). By default there is no timeout for complete calls, but * there is for the connect, write, and read actions within a call. / @get:JvmName("callTimeoutMillis") val callTimeoutMillis: Int = builder.callTimeout

/* Default connect timeout (in milliseconds). The default is 10 seconds. / @get:JvmName("connectTimeoutMillis") val connectTimeoutMillis: Int = builder.connectTimeout

/* Default read timeout (in milliseconds). The default is 10 seconds. / @get:JvmName("readTimeoutMillis") val readTimeoutMillis: Int = builder.readTimeout

/* Default write timeout (in milliseconds). The default is 10 seconds. / @get:JvmName("writeTimeoutMillis") val writeTimeoutMillis: Int = builder.writeTimeout

/* Web socket and HTTP/2 ping interval (in milliseconds). By default pings are not sent. / @get:JvmName("pingIntervalMillis") val pingIntervalMillis: Int = builder.pingInterval ```

其中需要額外瞭解下的是,最後一個 pingIntervalMillis。這是 HTTP2 的時候,可能是一個長連線,那麼需要通過這個來進行保活,客戶端發一個 ping,服務端回一個 pong


2.結語

這次的文章,主要是講了這些屬性的,以便我們在一些定製化需求上,知道我們可以用什麼來做。然後具體的用法,其實自己嘗試一下,或者搜一下就出來了。

如果文章中,有不懂的,或者紕漏,或者對我的文章寫作排版有建議和意見的,都可以評論或者私信我。

最後的最後,我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿