DEVLOG 12.30 網(wǎng)絡(luò)請求框架(上)-- OkHttp中的Dispatcher
參考內(nèi)容:?Android程序員中高級進(jìn)階學(xué)習(xí)/OkHttp原理分析

寫在前面:2021年就要結(jié)束了,這一年我從九月份開始寫專欄,我把這里當(dāng)成了自己的云上筆記,主要將B站的一些很好的學(xué)習(xí)視頻進(jìn)行總結(jié),或者是寫寫自己的開發(fā)心得,竟然最后寫了5w字,非常不可思議!感謝各位的閱讀(但是好像人也不多哈哈。不過無所謂啦,這個(gè)是今年的最后一篇文章。當(dāng)然,后續(xù)會繼續(xù)寫攔截器,以及OKHttp包裝之后的Retrofit,Retrofit真的是寫的非常出色! 還有對于阻塞隊(duì)列、并發(fā)的學(xué)習(xí)心得,不過目前還沒怎么學(xué)習(xí)?-?_?-

這篇文章主要的內(nèi)容:
分析OkHttpClient構(gòu)建Call(請求)的過程
分析構(gòu)建請求之后,OkHttp如果使用線程池API執(zhí)行請求。

首先通過常見的使用進(jìn)行源碼分析:
我們通常構(gòu)建一個(gè)OkHttpClient,通過OkHttp#newCall方法,結(jié)合我們構(gòu)建的Request對象,執(zhí)行execute(同步)或者enqueue(異步)進(jìn)行請求,所以我們首先看看OkHttp#newCall方法。
OkHttp#newCall
Call接口
RealCall是OkHttp定義的一個(gè)類,它繼承自Call。Call接口描述了一個(gè)準(zhǔn)備好被執(zhí)行的請求,我們可以通過Call的接口看看它能執(zhí)行那些操作:
OkHttp實(shí)現(xiàn)了這里的Factory,所以我們可以通過Factory工廠方法創(chuàng)建Call對象。
RealCall#newRealCall
此處的EventListener描述的是一個(gè)Http請求的過程。通常來說,一個(gè)Http請求,包括DNS解析(通過域名獲取IP地址),TCP/IP連接,然后再是應(yīng)用層的Http請求,我們可以通過這個(gè)類的方法略知一二。

所以再看完Call對象的構(gòu)建之后,我們來看另外兩個(gè)非常重要的方法,分別是enqueue和execute。
RealCall#enqueue
enqueue方法解釋了為什么OkHttp中的Call不能被執(zhí)行兩次。因?yàn)镃all也存在生命周期,已經(jīng)被執(zhí)行的話,還執(zhí)行一次會拋出異常。
此外,call被轉(zhuǎn)交給Dispatcher#enqueue方法。Dispatcher#enqueue方法會根據(jù)條件選擇是否立刻執(zhí)行當(dāng)前的call:
以上的條件主要是兩個(gè):
當(dāng)前正在執(zhí)行的Call的數(shù)量不能超過maxRequests(默認(rèn)定義為64)
對于同一個(gè)host的請你去不能超過maxRequestsPerHost(默認(rèn)定義為5)
跟蹤代碼發(fā)現(xiàn)這個(gè)host是使用HttpUrl類表示的,也就是我們常用的http url。
也就是說,如果滿足條件,就會將請求存儲到正在執(zhí)行的一步請求隊(duì)列(runningAsyncCalls),否則就存儲到就緒隊(duì)列(readyAsyncCalls)中:
readyAsyncCalls中的請求什么時(shí)候被執(zhí)行?
有一個(gè)非常明顯的事情,當(dāng)我們使用OkHttp執(zhí)行第一個(gè)請求的時(shí)候,這個(gè)請求肯定會被放入running隊(duì)列中,并且我們的Call對象其實(shí)還被包裝了一層AsyncCall:
我們的Call最后是要被送到線程池中執(zhí)行的,這意味著execute方法會被回調(diào),在execute方法中,攔截器幫助我們執(zhí)行了最重要的Http請求,并且返回給我們Repsonse。在finally塊里面,dispatcher調(diào)用了finish方法。當(dāng)一個(gè)請求完成的時(shí)候,勢必要去查看隊(duì)列中是否還有沒有被執(zhí)行的任務(wù):
Dispatcher可以通過promoteCalls,將ready隊(duì)列中的請求拉出來執(zhí)行:
這里會將ready隊(duì)列中的請求,在滿足不超過前面說的maxRequest和maxRequestPerHost的前提下取出來,放到running隊(duì)列中,然后交給executorService執(zhí)行。
彳亍,我們現(xiàn)在的目標(biāo)就是要研究一下線程池是如何執(zhí)行的我們的請求的。
使用線程池ExecutorService執(zhí)行call
Executor構(gòu)建了一個(gè)ThreadPoolExecutor作為線程池,一個(gè)線程池具有幾個(gè)比較重要的參數(shù):
核心線程數(shù),這里設(shè)定為0
最大線程數(shù),設(shè)定為max
線程存活時(shí)間。因?yàn)槲覀兌x在線程池中的線程的數(shù)量可能大于最大線程數(shù),如果大于最大線程數(shù),并且還是空閑狀態(tài)的這些線程,將會被移出。
任務(wù)隊(duì)列,任務(wù)隊(duì)列是一個(gè)BlockingQueue,它規(guī)定了任務(wù)的排隊(duì)方式。這里的實(shí)現(xiàn)是SynchronousQueue。
RealCall#execute
execute方法和enqueue方法類似,根據(jù)上述的分析可以很快弄明白:
為什么要使用SynchronousQueue?
為什么在OkHttp中非要使用SynchronousQueue作為阻塞隊(duì)列,使用fixed大小的隊(duì)列,比如ArrayBlockingQueue,LinkedBlockingQueue為啥不行捏?這一節(jié)主要想說明這個(gè)問題。
之所以要使用SynchronousQueue作為阻塞隊(duì)列,我們首先需要分析ThreadPoolExecutor執(zhí)行任務(wù)的特點(diǎn)。(關(guān)于不同的阻塞隊(duì)列的比較,我后面一定會寫文章來說明)。
在ThreadPoolExecutor#execute中,ctl這個(gè)量其實(shí)是workerCount和runState這兩個(gè)對于線程池【狀態(tài)】的一個(gè)組合:

ctl是一個(gè)AtomicInteger,它的32位被拆分成這樣兩個(gè)部分,前面三位用于表示這幾種狀態(tài):

workerCount表示線程池中的線程的數(shù)量,可以看到這個(gè)數(shù)量是非常龐大的,差不多有這么多:(2^29)-1 (about 500 million)。
ThreadPoolExecutor#execute
ok,稍微了解了ctl,我們來看看execute的代碼:
對于一個(gè)隨便的Runnable,我們首先會計(jì)算當(dāng)前workerCount和核心數(shù)量比較,如果小于核心數(shù)量,就使用addWorker創(chuàng)建新線程;否則,加入workerQueue。這里的workerQueue是我們指定的阻塞隊(duì)列。如果不能加入阻塞隊(duì)列(通常是因?yàn)槌^了固定的大小,比如ArrayBlockingQueue我們會指定大?。?,就會走到else if 分支,再調(diào)用addWorker創(chuàng)建新線程。 基于以上分析,我們上兩個(gè)例子。
Case#1 使用ArrayBlockingQueue,大小設(shè)置為1,其他的設(shè)置和OkHttp相同:
輸出結(jié)果:
并沒有輸出thread-2,如果按照上面的分析來看,因?yàn)閏ore大小為0,第一個(gè)if直接pass,然后在第二個(gè)分支中,現(xiàn)將任務(wù)加入workQueue,此時(shí)發(fā)現(xiàn)workerCount為0,立刻執(zhí)行任務(wù)1,workerQueue取出這個(gè)任務(wù);當(dāng)加入第二個(gè)任務(wù)時(shí),和上面類似,workerQueue存入任務(wù)2,但是workercount不是0,所以第二個(gè)任務(wù)就被擱置了。
Case#2,在加入一個(gè)任務(wù):
此時(shí),在加入一個(gè)任務(wù),在第三個(gè)任務(wù)進(jìn)來之前,和上面分析相同,出現(xiàn)第三個(gè)任務(wù),因?yàn)閣orkerQueue大小為1,存了第二個(gè)任務(wù),則第二個(gè)if沒有命中,會走下面的addWorker,創(chuàng)建一條新的線程,先執(zhí)行任務(wù)3,再執(zhí)行任務(wù)2,我們來看看輸出結(jié)果:
結(jié)果和我們分析的一樣,2 3 任務(wù)是在另外的線程池上執(zhí)行的。
通過這個(gè)現(xiàn)象,我們不難分析出使用ArrayBlockingQueue存在兩個(gè)顯著的局限性:
需要設(shè)置大小。如果前面的任務(wù)耗時(shí),后面的任務(wù)會存在隊(duì)列中,無法執(zhí)行
就算可以執(zhí)行了,執(zhí)行順序并非我們的預(yù)期。
基于以上兩點(diǎn)分析,我們就選擇了SynchronousQueue作為阻塞隊(duì)列。(但是為什么這個(gè)隊(duì)列可以保證順序,之后的文章會分析)。

所以!我們來小結(jié)一下本篇文章:
?我們通常使用的OkHttpClient是Call對象的一個(gè)工廠實(shí)現(xiàn)類,它通過newCall構(gòu)建Call對象。
構(gòu)建出的Call對象調(diào)用enqueue方法或者是execute方法時(shí),都會轉(zhuǎn)到Dispatcher的enqueue方法或者是execute方法中。
Dispatcher內(nèi)部使用了ThreadPoolExecutor作為線程池。該線程池的阻塞隊(duì)列實(shí)現(xiàn)使用了SynchronousQueue,使用這個(gè)隊(duì)列是為了保證請求的順序和請求都被執(zhí)行。