作者:RicardoMJiang 鏈接:https://juejin.cn/post/7020027832977850381
OkHttp 可以說是 Android 開發中最常見的網絡請求框架,OkHttp 使用方便,擴展性強,功能強大,OKHttp 源碼與原理也是面試中的常客。
但是 OKHttp 的源碼內容比較多,想要學習它的源碼往往千頭萬緒,一時抓不住重點. 本文從幾個問題出發梳理 OKHttp 相關知識點,以便快速構建 OKHttp 知識體,本文主要包括以下內容
首先來看一個最簡單的 Http 請求是如何發送的。
valokHttpClient=OkHttpClient()valrequest:Request=Request.Builder().url("https://www.google.com/").build()okHttpClient.newCall(request).enqueue(object:Callback{overridefunonFailure(call:Call,e:IOException){}overridefunonResponse(call:Call,response:Response){}})這段代碼看起來比較簡單,OkHttp 請求過程中最少只需要接觸 OkHttpClient、Request、Call、 Response,但是框架內部會進行大量的邏輯處理。
所有網絡請求的邏輯大部分集中在攔截器中,但是在進入攔截器之前還需要依靠分發器來調配請求任務。關於分發器與攔截器,我們在這裡先簡單介紹下,後續會有更加詳細的講解

整個網絡請求過程大致如上所示
分發器的主要作用是維護請求隊列與線程池,比如我們有100個異步請求,肯定不能把它們同時請求,而是應該把它們排隊分個類,分為正在請求中的列表和正在等待的列表, 等請求完成後,即可從等待中的列表中取出等待的請求,從而完成所有的請求
而這裡同步請求各異步請求又略有不同
同步請求synchronizedvoidexecuted(RealCallcall){runningSyncCalls.add(call);}因為同步請求不需要線程池,也不存在任何限制。所以分發器僅做一下記錄。後續按照加入隊列的順序同步請求即可
異步請求synchronizedvoidenqueue(AsyncCallcall){//請求數最大不超過64,同一Host請求不能超過5個if(runningAsyncCalls.size()<maxRequests&&runningCallsForHost(call)<maxRequestsPerHost){runningAsyncCalls.add(call);executorService().execute(call);}else{readyAsyncCalls.add(call);}}當正在執行的任務未超過最大限制64,同時同一 Host 的請求不超過5個,則會添加到正在執行隊列,同時提交給線程池。否則先加入等待隊列。每個任務完成後,都會調用分發器的 finished 方法,這裡面會取出等待隊列中的任務繼續執行
3. OKHttp攔截器是怎樣工作的?經過上面分發器的任務分發,下面就要利用攔截器開始一系列配置了
#RealCalloverridefunexecute():Response{try{client.dispatcher.executed(this)returngetResponseWithInterceptorChain()}finally{client.dispatcher.finished(this)}}我們再來看下 RealCall的execute方法,可以看出,最後返回了 getResponseWithInterceptorChain ,責任鏈的構建與處理其實就是在這個方法裡面
internalfungetResponseWithInterceptorChain():Response{//Buildafullstackofinterceptors.valinterceptors=mutableListOf<Interceptor>()interceptors+=client.interceptorsinterceptors+=RetryAndFollowUpInterceptor(client)interceptors+=BridgeInterceptor(client.cookieJar)interceptors+=CacheInterceptor(client.cache)interceptors+=ConnectInterceptorif(!forWebSocket){interceptors+=client.networkInterceptors}interceptors+=CallServerInterceptor(forWebSocket)valchain=RealInterceptorChain(call=this,interceptors=interceptors,index=0)valresponse=chain.proceed(originalRequest)}如上所示,構建了一個 OkHttp 攔截器的責任鏈
責任鏈,顧名思義,就是用來處理相關事務責任的一條執行鏈,執行鏈上有多個節點,每個節點都有機會(條件匹配)處理請求事務,如果某個節點處理完了就可以根據實際業務需求傳遞給下一個節點繼續處理或者返回處理完畢。
如上所示責任鏈添加的順序及作用如下表所示:
我們的網絡請求就是這樣經過責任鏈一級一級的遞推下去,最終會執行到 CallServerInterceptor的intercept 方法,此方法會將網絡響應的結果封裝成一個 Response 對象並 return。之後沿着責任鏈一級一級的回溯,最終就回到 getResponseWithInterceptorChain 方法的返回,如下圖所示:

從整個責任鏈路來看,應用攔截器是最先執行的攔截器,也就是用戶自己設置 request 屬性後的原始請求,而網絡攔截器位於 ConnectInterceptor 和 CallServerInterceptor 之間,此時網絡鏈路已經準備好,只等待發送請求數據。它們主要有以下區別
首先,應用攔截器在 RetryAndFollowUpInterceptor 和 CacheInterceptor 之前,所以一旦發生錯誤重試或者網絡重定向,網絡攔截器可能執行多次,因為相當於進行了二次請求,但是應用攔截器永遠只會觸發一次。另外如果在 CacheInterceptor 中命中了緩存就不需要走網絡請求了,因此會存在短路網絡攔截器的情況。
其次,除了 CallServerInterceptor 之外,每個攔截器都應該至少調用一次 realChain.proceed 方法。實際上在應用攔截器這層可以多次調用 proceed 方法(本地異常重試)或者不調用 proceed 方法(中斷),但是網絡攔截器這層連接已經準備好,可且僅可調用一次 proceed 方法。
最後,從使用場景看,應用攔截器因為只會調用一次,通常用於統計客戶端的網絡請求發起情況;而網絡攔截器一次調用代表了一定會發起一次網絡通信,因此通常可用於統計網絡鏈路上傳輸的數據。
ConnectInterceptor 的主要工作就是負責建立 TCP 連接,建立 TCP 連接需要經歷三次握手四次揮手等操作,如果每個 HTTP 請求都要新建一個 TCP 消耗資源比較多 而 Http1.1 已經支持 keep-alive ,即多個 Http 請求復用一個 TCP 連接,OKHttp 也做了相應的優化,下面我們來看下 OKHttp 是怎麼復用 TCP 連接的
ConnectInterceptor 中查找連接的代碼會最終會調用到 ExchangeFinder.findConnection 方法,具體如下:
#ExchangeFinder//為承載新的數據流尋找連接。尋找順序是已分配的連接、連接池、新建連接privateRealConnectionfindConnection(intconnectTimeout,intreadTimeout,intwriteTimeout,intpingIntervalMillis,booleanconnectionRetryEnabled)throwsIOException{synchronized(connectionPool){//1.嘗試使用已給數據流分配的連接.(例如重定向請求時,可以復用上次請求的連接)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. 現在有了IP地址,再次嘗試從連接池獲取。可能會因為連接合併而匹配。(這裡傳入了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.最後一次嘗試從連接池獲取,注意最後一個參數為true,即要求多路復用(http2.0)//意思是,如果本次是http2.0,那麼為了保證多路復用性,(因為上面的握手操作不是線程安全)會再次確認連接池中此時是否已有同樣連接if(connectionPool.transmitterAcquirePooledConnection(address,transmitter,routes,true)){//如果獲取到,就關閉我們創建里的連接,返回獲取的連接result=transmitter.connection;}else{//最後一次嘗試也沒有的話,就把剛剛新建的連接存入連接池connectionPool.put(result);}}returnresult;}上面精簡了部分代碼,可以看出,連接攔截器使用了5種方法查找連接
以上就是連接攔截器嘗試復用連接的操作,流程圖如下:

上面說到我們會建立一個 TCP 連接池,但如果沒有任務了,空閒的連接也應該及時清除,OKHttp 是如何做到的呢?
#RealConnectionPoolprivatevalcleanupQueue:TaskQueue=taskRunner.newQueue()privatevalcleanupTask=object:Task("$okHttpNameConnectionPool"){overridefunrunOnce():Long=cleanup(System.nanoTime())}longcleanup(longnow){intinUseConnectionCount=0;//正在使用的連接數intidleConnectionCount=0;//空閒連接數RealConnectionlongestIdleConnection=null;//空閒時間最長的連接longlongestIdleDurationNs=Long.MIN_VALUE;//最長的空閒時間//遍歷連接:找到待清理的連接, 找到下一次要清理的時間(還未到最大空閒時間)synchronized(this){for(Iterator<RealConnection>i=connections.iterator();i.hasNext();){RealConnectionconnection=i.next();//若連接正在使用,continue,正在使用連接數+1if(pruneAndGetAllocationCount(connection,now)>0){inUseConnectionCount++;continue;}//空閒連接數+1idleConnectionCount++;//賦值最長的空閒時間和對應連接longidleDurationNs=now-connection.idleAtNanos;if(idleDurationNs>longestIdleDurationNs){longestIdleDurationNs=idleDurationNs;longestIdleConnection=connection;}}//若最長的空閒時間大於5分鐘或空閒數大於5,就移除並關閉這個連接if(longestIdleDurationNs>=this.keepAliveDurationNs||idleConnectionCount>this.maxIdleConnections){connections.remove(longestIdleConnection);}elseif(idleConnectionCount>0){//else,就返回還剩多久到達5分鐘,然後wait這個時間再來清理returnkeepAliveDurationNs-longestIdleDurationNs;}elseif(inUseConnectionCount>0){//連接沒有空閒的,就5分鐘後再嘗試清理.returnkeepAliveDurationNs;}else{//沒有連接,不清理cleanupRunning=false;return-1;}}//關閉移除的連接closeQuietly(longestIdleConnection.socket());//關閉移除後立刻進行下一次的嘗試清理return0;}思路還是很清晰的:
流程如下圖所示:

END
推薦閱讀
Compose Multiplatform 實戰聯機小遊戲
入木三分:從設計者角度看Retrofit原理
最新出爐|2021 Android 面試被錘之旅
加好友進交流群,技術乾貨聊不停

