一、前言
如今面試中高級(jí)開(kāi)發(fā)工程師崗位,OKhttp 原理是必問(wèn)環(huán)節(jié),只會(huì)使用已經(jīng)無(wú)法滿足 Android 開(kāi)發(fā)市場(chǎng)的需求,優(yōu)秀的第三方框架源碼剖析不僅能深度理解框架,也能對(duì)自己學(xué)習(xí)帶來(lái)很大的幫助。
本篇文章根據(jù)朋友反饋和親身經(jīng)歷簡(jiǎn)單整理的一些關(guān)于 Okhttp 常見(jiàn)面試題目。
1.Okhttp 基本實(shí)現(xiàn)原理
OkHttp 主要是通過(guò) 5 個(gè)攔截器和 3 個(gè)雙端隊(duì)列(2 個(gè)異步隊(duì)列,1 個(gè)同步隊(duì)列)工作。內(nèi)部實(shí)現(xiàn)通過(guò)一個(gè)責(zé)任鏈模式完成,將網(wǎng)絡(luò)請(qǐng)求的各個(gè)階段封裝到各個(gè)鏈條中,實(shí)現(xiàn)了各層的解耦。
OkHttp 的底層是通過(guò) Socket 發(fā)送 HTTP 請(qǐng)求與接受響應(yīng),但是 OkHttp 實(shí)現(xiàn)了連接池的概念,即對(duì)于同一主機(jī)的多個(gè)請(qǐng)求,可以公用一個(gè) Socket 連接,而不是每次發(fā)送完 HTTP 請(qǐng)求就關(guān)閉底層的 Socket,這樣就實(shí)現(xiàn)了連接池的概念。而 OkHttp 對(duì) Socket 的讀寫(xiě)操作使用的 OkIo 庫(kù)進(jìn)行了一層封裝。
執(zhí)行流程:
- 通過(guò)構(gòu)建者構(gòu)建出OkHttpClient對(duì)象,再通過(guò)newCall方法獲得RealCall請(qǐng)求對(duì)象.
- 通過(guò)RealCall發(fā)起同步或異步請(qǐng)求,而決定是異步還是同步請(qǐng)求的是由線程分發(fā)器dispatcher來(lái)決定.
- 當(dāng)發(fā)起同步請(qǐng)求時(shí)會(huì)將請(qǐng)求加入到同步隊(duì)列中依次執(zhí)行,所以會(huì)阻塞UI線程,需要開(kāi)啟子線程執(zhí)行.
- 當(dāng)發(fā)起異步請(qǐng)求時(shí)會(huì)創(chuàng)建一個(gè)線程池,并且判斷請(qǐng)求隊(duì)列是否大于最大請(qǐng)求隊(duì)列64,請(qǐng)求主機(jī)數(shù)是否大于5,如果大于請(qǐng)求添加到異步等待隊(duì)列中,否則添加到異步執(zhí)行隊(duì)列,并執(zhí)行任務(wù).
2.Okhttp 網(wǎng)絡(luò)緩存如何實(shí)現(xiàn)?
OKHttp 默認(rèn)只支持 get 請(qǐng)求的緩存。
- 第一次拿到響應(yīng)后根據(jù)頭信息決定是否緩存。
- 下次請(qǐng)求時(shí)判斷是否存在本地緩存,是否需要使用對(duì)比緩存、封裝請(qǐng)求頭信息等等。
- 如果緩存失效或者需要對(duì)比緩存則發(fā)出網(wǎng)絡(luò)請(qǐng)求,否則使用本地緩存。
3.Okhttp 網(wǎng)絡(luò)連接怎么實(shí)現(xiàn)復(fù)用?
HttpEngine 在發(fā)起請(qǐng)求之前,會(huì)先調(diào)用nextConnection()來(lái)獲取一個(gè)Connection對(duì)象,如果可以從ConnectionPool中獲取一個(gè)Connection對(duì)象,就不會(huì)新建,如果無(wú)法獲取,就會(huì)調(diào)用createnextConnection()來(lái)新建一個(gè)Connection對(duì)象,這就是 Okhttp 多路復(fù)用的核心,不像之前的網(wǎng)絡(luò)框架,無(wú)論有沒(méi)有,都會(huì)新建Connection對(duì)象。

4.Dispatcher 的功能是什么?
Dispatcher中文是分發(fā)器的意思,和攔截器不同的是分發(fā)器不做事件處理,只做事件流向。他負(fù)責(zé)將每一次Requst進(jìn)行分發(fā),壓棧到自己的線程池,并通過(guò)調(diào)用者自己不同的方式進(jìn)行異步和同步處理。 通俗的講就是主要維護(hù)任務(wù)隊(duì)列的作用。
- 記錄同步任務(wù)、異步任務(wù)及等待執(zhí)行的異步任務(wù)。
- 調(diào)度線程池管理異步任務(wù)。
- 發(fā)起/取消網(wǎng)絡(luò)請(qǐng)求 API:execute、enqueue、cancel。
Dispatcher 類(lèi),該類(lèi)中維護(hù)了三個(gè)雙端隊(duì)列(Deque):
readyAsyncCalls:準(zhǔn)備運(yùn)行的異步請(qǐng)求
runningAsyncCalls:正在運(yùn)行的異步請(qǐng)求
runningSyncCalls:正在運(yùn)行的同步請(qǐng)求
OkHttp 設(shè)置了默認(rèn)的最大并發(fā)請(qǐng)求量 maxRequests = 64 和單個(gè) Host 主機(jī)支持的最大并發(fā)量 maxRequestsPerHost = 5
5.addInterceptor 與 addNetworkInterceptor 的區(qū)別?
二者通常的叫法為應(yīng)用攔截器和網(wǎng)絡(luò)攔截器,從整個(gè)責(zé)任鏈路來(lái)看,應(yīng)用攔截器是最先執(zhí)行的攔截器,也就是用戶自己設(shè)置request屬性后的原始請(qǐng)求,而網(wǎng)絡(luò)攔截器位于ConnectInterceptor和CallServerInterceptor之間,此時(shí)網(wǎng)絡(luò)鏈路已經(jīng)準(zhǔn)備好,只等待發(fā)送請(qǐng)求數(shù)據(jù)。
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ì)客戶端的網(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ù)。
6、Okhttp 攔截器的作用是什么?
1、應(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流。
2、網(wǎng)絡(luò)攔截器
用戶自定義攔截器,通常用于監(jiān)控網(wǎng)絡(luò)層的數(shù)據(jù)傳輸。
- CallServerInterceptor 請(qǐng)求攔截器,在前置準(zhǔn)備工作完成后,真正發(fā)起了網(wǎng)絡(luò)請(qǐng)求。
7、Okhttp 有哪些優(yōu)勢(shì)?
- 支持 http2,對(duì)一臺(tái)機(jī)器的所有請(qǐng)求共享同一個(gè) Socket
- 內(nèi)置連接池,支持連接復(fù)用,減少延遲
- 支持透明的 gzip 壓縮響應(yīng)體
- 響應(yīng)緩存可以完全避免網(wǎng)絡(luò)重復(fù)請(qǐng)求
- 請(qǐng)求失敗時(shí)自動(dòng)重試主機(jī)的其他 ip,自動(dòng)重定向
- 豐富的 API,可擴(kuò)展性好
8、response.body().string() 為什么只能調(diào)用一次?
我們可能習(xí)慣在獲取到Response對(duì)象后,先response.body().string()打印一遍 Log,再進(jìn)行數(shù)據(jù)解析,卻發(fā)現(xiàn)第二次直接拋異常,其實(shí)直接跟源碼進(jìn)去看就發(fā)現(xiàn),通過(guò)source拿到字節(jié)流以后,直接調(diào)用closeQuietly()方法關(guān)閉了,這樣第二次再去通過(guò)source讀取就直接流已關(guān)閉的異常了。
public final String string() throws IOException {
BufferedSource source = source();
try {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
} finally {
//這里講resource給悄悄close了
Util.closeQuietly(source);
}
}
解決方案:
1.內(nèi)存緩存一份response.body().string();
2.自定義攔截器處理 Log。
9、OkHttp請(qǐng)求整體流程是怎么樣?
- Request-》OkHttpClient-》RealCall
- 同步 -》 在調(diào)用線程 執(zhí)行五大攔截器
- 異步 -》 使用分發(fā)器將任務(wù)在線程池執(zhí)行 五大攔截器
var okHttpClient = OkHttpClient.Builder().build()
var request = Request.Builder().url("https://www.baidu.com")
.cacheControl(CacheControl.FORCE_CACHE)
.build()
var call = okHttpClient.newCall(request)
val result = call.execute()
println(result.isSuccessful)
result.close()

- 分發(fā)器:
內(nèi)部維護(hù)隊(duì)列與線程池,完成請(qǐng)求調(diào)配; - 攔截器:
完成整個(gè)請(qǐng)求過(guò)程。
10、分發(fā)器是如何工作的?
對(duì)于同步請(qǐng)求,分發(fā)器只記錄請(qǐng)求,用于判斷IdleRunnable是否需要執(zhí)行;對(duì)于異步請(qǐng)求,向分發(fā)器中提交請(qǐng)求;
- 同步-》記錄同步任務(wù):RealCall
- 異步-》首先將任務(wù)加入ready隊(duì)列等待執(zhí)行 -》是否需要將ready中的任務(wù)放入running 執(zhí)行
- 同時(shí)請(qǐng)求的異步任務(wù)數(shù)不得大于64個(gè)
- 從ready中取出來(lái)的異步任務(wù),與其相同的HOST,不得大于5個(gè)
- 若已經(jīng)存在5個(gè)相同HOST的任務(wù)在執(zhí)行,則繼續(xù)從ready中檢查下一個(gè)等待任務(wù)
Q:如何決定將請(qǐng)求放入ready還是running?
A:如果當(dāng)前正在請(qǐng)求數(shù)為64,則將請(qǐng)求放入ready等待執(zhí)行;如果小于64,但是已經(jīng)存在同一域名主機(jī)的請(qǐng)求5個(gè),也會(huì)放入ready;否則放入running隊(duì)列立即執(zhí)行;
Q:從ready移動(dòng)running的條件是什么?
A:每個(gè)請(qǐng)求執(zhí)行完成就會(huì)從running移除,同時(shí)進(jìn)行第一步相同邏輯的判斷,決定是否移動(dòng)!
11、攔截器是如何工作的?
- 責(zé)任鏈設(shè)計(jì)模式 將請(qǐng)求者 與 執(zhí)行者 解耦
- 讓請(qǐng)求者 只需要將請(qǐng)求發(fā)給責(zé)任鏈即可,無(wú)需關(guān)系請(qǐng)求過(guò)程與細(xì)節(jié)。
- 重試重定向、橋接、緩存、連接、請(qǐng)求服務(wù)
12、應(yīng)用攔截器與網(wǎng)絡(luò)攔截器的區(qū)別?
OkHttp中攔截器有:自定義應(yīng)用攔截器、重試重定向、橋接、緩存、連接、自定義網(wǎng)絡(luò)攔截器、請(qǐng)求服務(wù)。
自定義應(yīng)用攔截器與自定義網(wǎng)絡(luò)攔截器的區(qū)別主要是順序的區(qū)別,由于以上攔截器采用責(zé)任鏈設(shè)計(jì)模式組合執(zhí)行,因此順序不同,帶來(lái)的影響是:
應(yīng)用攔截器不需要關(guān)心是否重定向或者失敗重連(只會(huì)執(zhí)行一次);同時(shí)它也能決定是否執(zhí)行其他攔截器。而網(wǎng)絡(luò)攔截器則可以操作重定向與重試并且可能不會(huì)執(zhí)行(直接在緩存中獲得結(jié)果),同時(shí)它也可以觀察到真正的Request以及相關(guān)的連接信息(經(jīng)過(guò)其他攔截器處理完畢后)。
13、OkHttp緩存機(jī)制
OkHttp基于Http協(xié)議實(shí)現(xiàn)了緩存,但是
默認(rèn)是關(guān)閉狀態(tài),需要在配置OkHttpClient時(shí)候使用:OkHttpClient.Builder().cache(Cache(文件,大小)) .build() 開(kāi)啟。OkHttp只會(huì)緩存GET請(qǐng)求的響應(yīng),在RFC7231中GET,
HEAD和某些情況下的POST都是可緩存的,但是絕大多數(shù)的實(shí)現(xiàn)里只支持GET和HEAD的緩存,這是因?yàn)閜ost做的一般是修改和刪除的工作,所以必須與服務(wù)端交互,所以不能使用緩存。
而Http的緩存又分為強(qiáng)緩存與協(xié)商緩存。
14、強(qiáng)緩存
- 命中強(qiáng)緩存時(shí),瀏覽器并不會(huì)將請(qǐng)求發(fā)送給服務(wù)器。強(qiáng)緩存是利用http的返回頭中的Expires或者Cache- Control兩個(gè)字段來(lái)控制的,用來(lái)表示資源的緩存時(shí)間;
- 若未命中強(qiáng)緩存,則瀏覽器會(huì)將請(qǐng)求發(fā)送至服務(wù)器。服務(wù)器根據(jù)http頭信息中的Last-Modify/If-Modify- Since或Etag/If-None-Match來(lái)判斷是否命中協(xié)商緩存。如果命中,則http返回碼為304,客戶端從緩存中加載資源。
expires,它的值為一個(gè)絕對(duì)時(shí)間,如果發(fā)送請(qǐng)求的時(shí)間在expires之前,那么本地緩存有效,能夠直接使用緩存。
-
cache-control:max-age=number,資源第一次的請(qǐng)求時(shí)間和該值相加,計(jì)算出一個(gè)資源過(guò)期時(shí)間,再拿這個(gè)過(guò)期時(shí)間跟當(dāng)前的請(qǐng)求時(shí)間比較,如果請(qǐng)求時(shí)間在過(guò)期時(shí)間之前,就能命中緩存,否則就不行。另外此響應(yīng)頭還能設(shè)置為:
- no-cache:不使用本地緩存。
- no-store:不允許被緩存
- public:可以被任何用戶緩存,包括終端用戶和CDN等中間代理服務(wù)器。
- private:只能被終端用戶緩存,不允許CDN等中間代理服務(wù)器緩存。
- immutable:(響應(yīng))資源不會(huì)改變
15、協(xié)商緩存
協(xié)商緩存的意思是:瀏覽器會(huì)將請(qǐng)求發(fā)送至服務(wù)器。服務(wù)端可能響應(yīng)304(不包含響應(yīng)體數(shù)據(jù))表示緩存可用,可能正常響應(yīng),如200同時(shí)攜帶響應(yīng)體。為了讓服務(wù)端判斷是否可用緩存,在請(qǐng)求時(shí),需要攜帶標(biāo)識(shí):**If-Modified-Since或者If-None-Match**。
其中If-Modified-Since需要和響應(yīng)的Last-Modified 配合使用,而If-None-Match 則與Etag 配合。
Last-Modified/ If-Modified-Since
在緩存的響應(yīng)中響應(yīng)頭包含:Last-Modified ,表示服務(wù)端告知的對(duì)應(yīng)請(qǐng)求的資源在服務(wù)器上的最后修改時(shí)間。再次發(fā)起請(qǐng)求,需要在請(qǐng)求頭中攜帶:If-Modified-Since。其值就是響應(yīng)中的Last-Modified的值。意思就是告訴服務(wù)端我的緩存,在服務(wù)端什么時(shí)候修改后拿到的。服務(wù)端判斷這個(gè)時(shí)間后是否修改過(guò)資源,未修改則返回3-04,否則正常返回響應(yīng)數(shù)據(jù)。
Etag/If-None-Match
這兩個(gè)值是由服務(wù)器生成的每個(gè)資源的唯一標(biāo)識(shí)字符串,只要資源有變化就這個(gè)值就會(huì)改變。在請(qǐng)求時(shí)攜帶If-None-Match,其值是緩存的響應(yīng)頭中的Etag,服務(wù)端獲取到請(qǐng)求頭中的If-None-Match會(huì)重新計(jì)算此次請(qǐng)求資源的標(biāo)識(shí),如果一致則返回304。
16、Okhttp 運(yùn)用了哪些設(shè)計(jì)模式?
Okhttp 運(yùn)用了六種設(shè)計(jì)模式:
- 構(gòu)造者模式(OkhttpClient,Request 等各種對(duì)象的創(chuàng)建)
- 工廠模式(在 Call 接口中,有一個(gè)內(nèi)部工廠 Factory 接口。)
- 單例模式(Platform 類(lèi),已經(jīng)使用 Okhttp 時(shí)使用單例)
- 策略模式(在 CacheInterceptor 中,在響應(yīng)數(shù)據(jù)的選擇中使用了策略模式,選擇緩存數(shù)據(jù)還是選擇網(wǎng)絡(luò)訪問(wèn)。)
- 責(zé)任鏈模式(攔截器的鏈?zhǔn)秸{(diào)用)
- 享元模式(Dispatcher 的線程池中,不限量的線程池實(shí)現(xiàn)了對(duì)象復(fù)用)
17、HTTP1和HTTP2的區(qū)別
- 1.新的二進(jìn)制格式:HTTP2采用二進(jìn)制格式而HTTP1使用文本格式。
- 2.多路復(fù)用:HTTP2是完全多復(fù)用的,而非有序并阻塞的,只需一個(gè)連接即可實(shí)現(xiàn)并行。HTTP1一個(gè)連接只能發(fā)送一個(gè)請(qǐng)求。
- 3.頭部壓縮:HTTP 1.1中,每一次發(fā)送和響應(yīng),都有HTTP頭信息。HTTP 2壓縮頭信息,減少帶寬。
- 4.服務(wù)器推送:HTTP 1只能客戶端發(fā)送數(shù)據(jù),服務(wù)器端返回?cái)?shù)據(jù)。HTTP2中,服務(wù)器可以主動(dòng)向客戶端發(fā)起一些數(shù)據(jù)傳輸(如css和png等),服務(wù)器可以并行發(fā)送html,css,js等數(shù)據(jù)。。
18、為什么需要頭部壓縮?
HTTP協(xié)議是不帶有狀態(tài)的,每次請(qǐng)求頭部都會(huì)附上所有的信息,而且很多的信息都是重復(fù)的,這會(huì)浪費(fèi)很多寬帶也會(huì)影響速度,所以HTTP2對(duì)頭部進(jìn)行了壓縮,一方面使用gzip或compress進(jìn)行頭部壓縮,另一方面,客戶端和服務(wù)器會(huì)同時(shí)維護(hù)同一張頭信息表,所有的字段都會(huì)存入這張表中,生成一個(gè)索引號(hào),以后就不需要再發(fā)送同樣的字段了,只發(fā)送索引號(hào),提示了速度。