面試突擊:OkHttp 原理八連問
OkHttp 可以說是 Android 開發(fā)中最常見的網(wǎng)絡請求框架,OkHttp 使用方便,擴展性強,功能強大,OKHttp 源碼與原理也是面試中的???。
但是 OKHttp 的源碼內容比較多,想要學習它的源碼往往千頭萬緒,一時抓不住重點. 本文從幾個問題出發(fā)梳理 OKHttp 相關知識點,以便快速構建 OKHttp 知識體,本文主要包括以下內容
- OKHttp 請求的整體流程是怎樣的?
 - OKHttp 分發(fā)器是怎樣工作的?
 - OKHttp 攔截器是如何工作的?
 - 應用攔截器和網(wǎng)絡攔截器有什么區(qū)別?
 - OKHttp 如何復用 TCP 連接?
 - OKHttp 空閑連接如何清除?
 - OKHttp 有哪些優(yōu)點?
 - OKHttp 框架中用到了哪些設計模式?
 
1. OKHttp請求整體流程介紹
首先來看一個最簡單的 Http 請求是如何發(fā)送的。
- val okHttpClient = OkHttpClient()
 - val request: RequestRequest = Request.Builder()
 - .url("https://www.google.com/")
 - .build()
 - okHttpClient.newCall(request).enqueue(object :Callback{
 - override fun onFailure(call: Call, e: IOException) {
 - }
 - override fun onResponse(call: Call, response: Response) {
 - }
 - })
 
這段代碼看起來比較簡單,OkHttp 請求過程中最少只需要接觸 OkHttpClient、Request、Call、 Response,但是框架內部會進行大量的邏輯處理。
所有網(wǎng)絡請求的邏輯大部分集中在攔截器中,但是在進入攔截器之前還需要依靠分發(fā)器來調配請求任務。關于分發(fā)器與攔截器,我們在這里先簡單介紹下,后續(xù)會有更加詳細的講解
- 分發(fā)器:內部維護隊列與線程池,完成請求調配;
 - 攔截器:五大默認攔截器完成整個請求過程。
 
整個網(wǎng)絡請求過程大致如上所示
- 通過建造者模式構建 OKHttpClient 與 Request
 - OKHttpClient 通過 newCall 發(fā)起一個新的請求
 - 通過分發(fā)器維護請求隊列與線程池,完成請求調配
 - 通過五大默認攔截器完成請求重試,緩存處理,建立連接等一系列操作
 - 得到網(wǎng)絡請求結果
 
2. OKHttp分發(fā)器是怎樣工作的?
分發(fā)器的主要作用是維護請求隊列與線程池,比如我們有100個異步請求,肯定不能把它們同時請求,而是應該把它們排隊分個類,分為正在請求中的列表和正在等待的列表, 等請求完成后,即可從等待中的列表中取出等待的請求,從而完成所有的請求
而這里同步請求各異步請求又略有不同
同步請求
- synchronized void executed(RealCall call) {
 - runningSyncCalls.add(call);
 - }
 
因為同步請求不需要線程池,也不存在任何限制。所以分發(fā)器僅做一下記錄。后續(xù)按照加入隊列的順序同步請求即可
異步請求
- synchronized void enqueue(AsyncCall call) {
 - //請求數(shù)最大不超過64,同一Host請求不能超過5個
 - if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
 - runningAsyncCalls.add(call);
 - executorService().execute(call);
 - } else {
 - readyAsyncCalls.add(call);
 - }
 - }
 
當正在執(zhí)行的任務未超過最大限制64,同時同一 Host 的請求不超過5個,則會添加到正在執(zhí)行隊列,同時提交給線程池。否則先加入等待隊列。每個任務完成后,都會調用分發(fā)器的 finished 方法,這里面會取出等待隊列中的任務繼續(xù)執(zhí)行
3. OKHttp攔截器是怎樣工作的?
經(jīng)過上面分發(fā)器的任務分發(fā),下面就要利用攔截器開始一系列配置了
- # RealCall
 - override fun execute(): Response {
 - try {
 - client.dispatcher.executed(this)
 - return getResponseWithInterceptorChain()
 - } finally {
 - client.dispatcher.finished(this)
 - }
 - }
 
我們再來看下 RealCall的execute方法,可以看出,最后返回了 getResponseWithInterceptorChain ,責任鏈的構建與處理其實就是在這個方法里面
- internal fun getResponseWithInterceptorChain(): Response {
 - // Build a full stack of interceptors.
 - 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(
 - call = this,interceptorsinterceptors = interceptors,index = 0
 - )
 - val response = chain.proceed(originalRequest)
 - }
 
如上所示,構建了一個 OkHttp 攔截器的責任鏈
責任鏈,顧名思義,就是用來處理相關事務責任的一條執(zhí)行鏈,執(zhí)行鏈上有多個節(jié)點,每個節(jié)點都有機會(條件匹配)處理請求事務,如果某個節(jié)點處理完了就可以根據(jù)實際業(yè)務需求傳遞給下一個節(jié)點繼續(xù)處理或者返回處理完畢。
如上所示責任鏈添加的順序及作用,如下表所示:
| 攔截器 | 作用 | 
|---|---|
| 應用攔截器 | 拿到的是原始請求,可以添加一些自定義 header、通用參數(shù)、參數(shù)加密、網(wǎng)關接入等等。 | 
| RetryAndFollowUpInterceptor | 處理錯誤重試和重定向 | 
| BridgeInterceptor | 應用層和網(wǎng)絡層的橋接攔截器,主要工作是為請求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存響應結果的cookie,如果響應使用gzip壓縮過,則還需要進行解壓。 | 
| CacheInterceptor | 緩存攔截器,如果命中緩存則不會發(fā)起網(wǎng)絡請求。 | 
| ConnectInterceptor | 連接攔截器,內部會維護一個連接池,負責連接復用、創(chuàng)建連接(三次握手等等)、釋放連接以及創(chuàng)建連接上的socket流。 | 
| networkInterceptors(網(wǎng)絡攔截器) | 用戶自定義攔截器,通常用于監(jiān)控網(wǎng)絡層的數(shù)據(jù)傳輸。 | 
| CallServerInterceptor | 請求攔截器,在前置準備工作完成后,真正發(fā)起了網(wǎng)絡請求。 | 
我們的網(wǎng)絡請求就是這樣經(jīng)過責任鏈一級一級的遞推下去,最終會執(zhí)行到 CallServerInterceptor的intercept 方法,此方法會將網(wǎng)絡響應的結果封裝成一個 Response 對象并 return。之后沿著責任鏈一級一級的回溯,最終就回到 getResponseWithInterceptorChain 方法的返回,如下圖所示:
4. 應用攔截器和網(wǎng)絡攔截器有什么區(qū)別?
從整個責任鏈路來看,應用攔截器是最先執(zhí)行的攔截器,也就是用戶自己設置 request 屬性后的原始請求,而網(wǎng)絡攔截器位于 ConnectInterceptor 和 CallServerInterceptor 之間,此時網(wǎng)絡鏈路已經(jīng)準備好,只等待發(fā)送請求數(shù)據(jù)。它們主要有以下區(qū)別
1. 首先,應用攔截器在 RetryAndFollowUpInterceptor 和 CacheInterceptor 之前,所以一旦發(fā)生錯誤重試或者網(wǎng)絡重定向,網(wǎng)絡攔截器可能執(zhí)行多次,因為相當于進行了二次請求,但是應用攔截器永遠只會觸發(fā)一次。另外如果在 CacheInterceptor 中命中了緩存就不需要走網(wǎng)絡請求了,因此會存在短路網(wǎng)絡攔截器的情況。
2. 其次,除了 CallServerInterceptor 之外,每個攔截器都應該至少調用一次 realChain.proceed 方法。實際上在應用攔截器這層可以多次調用 proceed 方法(本地異常重試)或者不調用 proceed 方法(中斷),但是網(wǎng)絡攔截器這層連接已經(jīng)準備好,可且僅可調用一次 proceed 方法。
3. 最后,從使用場景看,應用攔截器因為只會調用一次,通常用于統(tǒng)計客戶端的網(wǎng)絡請求發(fā)起情況;而網(wǎng)絡攔截器一次調用代表了一定會發(fā)起一次網(wǎng)絡通信,因此通常可用于統(tǒng)計網(wǎng)絡鏈路上傳輸?shù)臄?shù)據(jù)。
5. OKHttp如何復用TCP連接?
ConnectInterceptor 的主要工作就是負責建立 TCP 連接,建立 TCP 連接需要經(jīng)歷三次握手四次揮手等操作,如果每個 HTTP 請求都要新建一個 TCP 消耗資源比較多 而 Http1.1 已經(jīng)支持 keep-alive ,即多個 Http 請求復用一個 TCP 連接,OKHttp 也做了相應的優(yōu)化,下面我們來看下 OKHttp 是怎么復用 TCP 連接的
ConnectInterceptor 中查找連接的代碼會最終會調用到 ExchangeFinder.findConnection 方法,具體如下:
- # ExchangeFinder
 - //為承載新的數(shù)據(jù)流 尋找 連接。尋找順序是 已分配的連接、連接池、新建連接
 - private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
 - int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
 - synchronized (connectionPool) {
 - // 1.嘗試使用 已給數(shù)據(jù)流分配的連接.(例如重定向請求時,可以復用上次請求的連接)
 - releasedConnection = transmitter.connection;
 - result = transmitter.connection;
 - if (result == null) {
 - // 2. 沒有已分配的可用連接,就嘗試從連接池獲取。(連接池稍后詳細講解)
 - if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
 - result = transmitter.connection;
 - }
 - }
 - }
 - synchronized (connectionPool) {
 - if (newRouteSelection) {
 - //3. 現(xiàn)在有了IP地址,再次嘗試從連接池獲取??赡軙驗檫B接合并而匹配。(這里傳入了routes,上面的傳的null)
 - routes = routeSelection.getAll();
 - if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
 - foundPooledConnection = true;
 - result = transmitter.connection;
 - }
 - }
 - // 4.第二次沒成功,就把新建的連接,進行TCP + TLS 握手,與服務端建立連接. 是阻塞操作
 - result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
 - connectionRetryEnabled, call, eventListener);
 - synchronized (connectionPool) {
 - // 5. 最后一次嘗試從連接池獲取,注意最后一個參數(shù)為true,即要求 多路復用(http2.0)
 - //意思是,如果本次是http2.0,那么為了保證 多路復用性,(因為上面的握手操作不是線程安全)會再次確認連接池中此時是否已有同樣連接
 - if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
 - // 如果獲取到,就關閉我們創(chuàng)建里的連接,返回獲取的連接
 - result = transmitter.connection;
 - } else {
 - //最后一次嘗試也沒有的話,就把剛剛新建的連接存入連接池
 - connectionPool.put(result);
 - }
 - }
 - return result;
 - }
 
上面精簡了部分代碼,可以看出,連接攔截器使用了5種方法查找連接:
- 首先會嘗試使用 已給請求分配的連接。(已分配連接的情況例如重定向時的再次請求,說明上次已經(jīng)有了連接)
 - 若沒有已分配的可用連接,就嘗試從連接池中 匹配獲取。因為此時沒有路由信息,所以匹配條件:address 一致—— host、port、代理等一致,且匹配的連接可以接受新的請求。
 - 若從連接池沒有獲取到,則傳入 routes 再次嘗試獲取,這主要是針對 Http2.0 的一個操作, Http2.0 可以復用 square.com 與 square.ca 的連接
 - 若第二次也沒有獲取到,就創(chuàng)建 RealConnection 實例,進行 TCP + TLS 握手,與服務端建立連接。
 - 此時為了確保 Http2.0 連接的多路復用性,會第三次從連接池匹配。因為新建立的連接的握手過程是非線程安全的,所以此時可能連接池新存入了相同的連接。
 - 第三次若匹配到,就使用已有連接,釋放剛剛新建的連接;若未匹配到,則把新連接存入連接池并返回。
 
以上就是連接攔截器嘗試復用連接的操作,流程圖如下:
6. OKHttp空閑連接如何清除?
上面說到我們會建立一個 TCP 連接池,但如果沒有任務了,空閑的連接也應該及時清除,OKHttp 是如何做到的呢?
- # RealConnectionPool
 - private val cleanupQueue: TaskQueue = taskRunner.newQueue()
 - private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
 - override fun runOnce(): Long = cleanup(System.nanoTime())
 - }
 - long cleanup(long now) {
 - int inUseConnectionCount = 0;//正在使用的連接數(shù)
 - int idleConnectionCount = 0;//空閑連接數(shù)
 - RealConnection longestIdleConnection = null;//空閑時間最長的連接
 - long longestIdleDurationNs = Long.MIN_VALUE;//最長的空閑時間
 - //遍歷連接:找到待清理的連接, 找到下一次要清理的時間(還未到最大空閑時間)
 - synchronized (this) {
 - for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
 - RealConnection connection = i.next();
 - //若連接正在使用,continue,正在使用連接數(shù)+1
 - if (pruneAndGetAllocationCount(connection, now) > 0) {
 - inUseConnectionCount++;
 - continue;
 - }
 - //空閑連接數(shù)+1
 - idleConnectionCount++;
 - // 賦值最長的空閑時間和對應連接
 - long idleDurationNs = now - connection.idleAtNanos;
 - if (idleDurationNs > longestIdleDurationNs) {
 - longestIdleDurationNs = idleDurationNs;
 - longestIdleConnection = connection;
 - }
 - }
 - //若最長的空閑時間大于5分鐘 或 空閑數(shù) 大于5,就移除并關閉這個連接
 - if (longestIdleDurationNs >= this.keepAliveDurationNs
 - || idleConnectionCount > this.maxIdleConnections) {
 - connections.remove(longestIdleConnection);
 - } else if (idleConnectionCount > 0) {
 - // else,就返回 還剩多久到達5分鐘,然后wait這個時間再來清理
 - return keepAliveDurationNs - longestIdleDurationNs;
 - } else if (inUseConnectionCount > 0) {
 - //連接沒有空閑的,就5分鐘后再嘗試清理.
 - return keepAliveDurationNs;
 - } else {
 - // 沒有連接,不清理
 - cleanupRunning = false;
 - return -1;
 - }
 - }
 - //關閉移除的連接
 - closeQuietly(longestIdleConnection.socket());
 - //關閉移除后 立刻 進行下一次的 嘗試清理
 - return 0;
 - }
 
思路還是很清晰的:
1. 在將連接加入連接池時就會啟動定時任務
2. 有空閑連接的話,如果最長的空閑時間大于5分鐘 或 空閑數(shù) 大于5,就移除關閉這個最長空閑連接;如果 空閑數(shù) 不大于5 且 最長的空閑時間不大于5分鐘,就返回到5分鐘的剩余時間,然后等待這個時間再來清理。
3. 沒有空閑連接就等5分鐘后再嘗試清理。
4. 沒有連接不清理。
流程如下圖所示:
7. OKHttp有哪些優(yōu)點?
- 使用簡單,在設計時使用了外觀模式,將整個系統(tǒng)的復雜性給隱藏起來,將子系統(tǒng)接口通過一個客戶端 OkHttpClient 統(tǒng)一暴露出來。
 - 擴展性強,可以通過自定義應用攔截器與網(wǎng)絡攔截器,完成用戶各種自定義的需求
 - 功能強大,支持 Spdy、Http1.X、Http2、以及 WebSocket 等多種協(xié)議
 - 通過連接池復用底層 TCP(Socket),減少請求延時
 - 無縫的支持 GZIP 減少數(shù)據(jù)流量
 - 支持數(shù)據(jù)緩存,減少重復的網(wǎng)絡請求
 - 支持請求失敗自動重試主機的其他 ip,自動重定向
 
8. OKHttp框架中用到了哪些設計模式?
- 構建者模式:OkHttpClient 與 Request 的構建都用到了構建者模式
 - 外觀模式:OkHttp使用了外觀模式,將整個系統(tǒng)的復雜性給隱藏起來,將子系統(tǒng)接口通過一個客戶端 OkHttpClient 統(tǒng)一暴露出來
 - 責任鏈模式: OKHttp 的核心就是責任鏈模式,通過5個默認攔截器構成的責任鏈完成請求的配置
 - 享元模式: 享元模式的核心即池中復用, OKHttp 復用 TCP 連接時用到了連接池,同時在異步請求中也用到了線程池
 



















 
 
 












 
 
 
 