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