前言
手把手講解系列文章缚够,是我寫給各位看官,也是寫給我自己的鹦赎。
文章可能過分詳細(xì)谍椅,但是這是為了幫助到盡量多的人,畢竟工作5,6年古话,不能老吸血雏吭,也到了回饋開源的時(shí)候.
這個(gè)系列的文章:
1、用通俗易懂的講解方式陪踩,講解一門技術(shù)的實(shí)用價(jià)值
2杖们、詳細(xì)書寫源碼的追蹤,源碼截圖肩狂,繪制類的結(jié)構(gòu)圖摘完,盡量詳細(xì)地解釋原理的探索過程
3、提供Github 的 可運(yùn)行的Demo工程,但是我所提供代碼傻谁,更多是提供思路孝治,拋磚引玉,請(qǐng)酌情cv
4审磁、集合整理原理探索過程中的一些坑谈飒,或者demo的運(yùn)行過程中的注意事項(xiàng)
5、用gif圖态蒂,最直觀地展示demo運(yùn)行效果如果覺得細(xì)節(jié)太細(xì)杭措,直接跳過看結(jié)論即可。
本人能力有限钾恢,如若發(fā)現(xiàn)描述不當(dāng)之處手素,歡迎留言批評(píng)指正。
學(xué)到老活到老瘩蚪,路漫漫其修遠(yuǎn)兮泉懦。與眾君共勉 !
引子
OkHttp 知名第三方網(wǎng)絡(luò)框架SDK,使用簡(jiǎn)單募舟,性能優(yōu)秀祠斧,但是內(nèi)核并不簡(jiǎn)單,此系列文章拱礁,專挑硬核知識(shí)點(diǎn)詳細(xì)講解.
何為硬核琢锋,就是要想深入研究辕漂,你絕對(duì)繞不過去的知識(shí)點(diǎn)
正文大綱
OkHttp是什么
OkHttp怎么用
OkHttp源碼核心類之一:分發(fā)器詳解
OkHttp源碼核心類之一:攔截器簡(jiǎn)述
正文
OkHttp是什么
OkHttp是時(shí)下非常流行的網(wǎng)絡(luò)編程框架,由行業(yè)巨佬
Square
公司開源,很多其他的流行框架比如retrofit
的底層也是okhttp吴超,只不過使用了注解反射動(dòng)態(tài)代理將其進(jìn)行了封裝钉嘹。
流行版本為:3.10.0,最新版本為:4.0.1鲸阻,只不過將實(shí)現(xiàn)語(yǔ)言從java改成了kotlin跋涣。
相對(duì)于其他網(wǎng)絡(luò)框架,有如下優(yōu)點(diǎn):
- 支持
Spdy
鸟悴、Http1.X
陈辱、Http2
、Quic
以及WebSocket
- 連接池復(fù)用底層
TCP(Socket)
细诸,減少請(qǐng)求延時(shí)- 無縫的支持
GZIP
減少數(shù)據(jù)流量- 緩存響應(yīng)數(shù)據(jù)減少重復(fù)的網(wǎng)絡(luò)請(qǐng)求
- 請(qǐng)求失敗自動(dòng)重試主機(jī)的其他
ip
沛贪,自動(dòng)重定向
OkHttp怎么用
添加gradle依賴
dependencies {
....
implementation("com.squareup.okhttp3:okhttp:4.0.1")
}
Java調(diào)用(同步請(qǐng)求,異步請(qǐng)求)
public class MyRequest {
/**
* 異步請(qǐng)求
*/
public void sendReqAsync() {
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url("http://www.baidu.com").build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.d("sendReqTag", "onFailure:" + e.getLocalizedMessage());
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
String s = new String().concat(response.code() + "\n")
.concat(response.message() + "\n")
.concat(response.body().string());
Log.d("sendReqTag", "onSuccess\n " + s);
}
});
}
/**
* 同步請(qǐng)求
*/
public void sendReqSync() {
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url("http://www.baidu.com").build();
Call call = client.newCall(request);
try {
Response response = call.execute();
String s = new String().concat(response.code() + "\n")
.concat(response.message() + "\n")
.concat(response.body().string());
Log.d("sendReqTag", "onSuccess\n " + s);
} catch (IOException e) {
e.printStackTrace();
}
}
}
OkHttp的簡(jiǎn)單使用方法大致使用如上震贵,其中也有一些細(xì)節(jié)需要注意:
1利赋、在應(yīng)用層使用OkHttp,必然會(huì)涉及到4個(gè)重要元素:
- OkHttpClient類(產(chǎn)生OkHttp客戶端實(shí)例)
- Request類(請(qǐng)求封裝)
- Call類(網(wǎng)絡(luò)任務(wù)封裝,并決定是要
同步執(zhí)行
還是異步執(zhí)行
,注意猩系,同步請(qǐng)求不
可以放在主線程
中媚送,但是異步請(qǐng)求可以 )- Response類(網(wǎng)絡(luò)任務(wù)執(zhí)行之后的回調(diào))
2、執(zhí)行網(wǎng)絡(luò)請(qǐng)求必須在manifest中申請(qǐng)
INTERNET
權(quán)限寇甸,不然會(huì)拋異常.
3塘偎、完整的一個(gè)請(qǐng)求執(zhí)行出去,流程如下圖:
OkHttp源碼核心類之一:分發(fā)器詳解
上述幽纷,提到Call類式塌,可以選擇性執(zhí)行 同步或者異步請(qǐng)求博敬,但是無論同步異步友浸,都一定會(huì)經(jīng)過一個(gè)門戶:"分發(fā)器" :
索引進(jìn)源碼(okhttp v3.10.0):
雖然用戶不需要直接操作分發(fā)器,但是 分發(fā)器偏窝,作為OkHttp
架構(gòu)的一個(gè)門戶層收恢,是所有請(qǐng)求的必經(jīng)之路,其中的代碼還是有必要了解細(xì)節(jié)的祭往。
同步請(qǐng)求
進(jìn)入分發(fā)器 Dispatcher
之后, 會(huì)執(zhí)行 getResponseWithInterceptorChain()
來執(zhí)行這個(gè)Call
任務(wù)伦意,得到一個(gè)Response
,其中的細(xì)節(jié)分為兩步:
1、
client.dispatcher().executed(this);
硼补,進(jìn)入源碼可以看到 僅僅是執(zhí)行了runningSyncCalls.add(call);
驮肉,將call對(duì)象加入到了一個(gè)雙端隊(duì)列Deque<RealCall> runningSyncCalls
中。
2已骇、getResponseWithInterceptorChain()
是執(zhí)行網(wǎng)絡(luò)請(qǐng)求的核心內(nèi)容离钝,涉及到攔截器票编,在這一節(jié)上暫時(shí)不詳述。
同步請(qǐng)求的執(zhí)行步驟十分簡(jiǎn)單卵渴,將任務(wù)加入到 runningSyncCalls列表慧域,并且直接執(zhí)行核心方法,同步阻塞拿到response浪读。
異步請(qǐng)求
異步請(qǐng)求進(jìn)入分發(fā)器之后昔榴,
可能會(huì)被加入到
Deque<AsyncCall> runningAsyncCalls
這么一個(gè)雙端隊(duì)列中,然后executorService().execute(call);
實(shí)際上是用了線程池來執(zhí)行了這個(gè)異步任務(wù)碘橘。
但是互订,請(qǐng)注意(還是剛才的enqueue方法代碼)這里有一個(gè)判斷條件 if分支 :
這個(gè)條件是否滿足,將會(huì)直接決定是直接執(zhí)行這個(gè)任務(wù)痘拆,還是將任務(wù)加入到 readyAsyncCalls 雙端隊(duì)列.
那么設(shè)置這個(gè)條件的目的是什么呢屁奏?從變量命名來看:
runningAsyncCalls 執(zhí)行中的異步任務(wù)
runningCallsForHost 同一個(gè)域名正在執(zhí)行的任務(wù)數(shù)
readyAsyncCalls 預(yù)備執(zhí)行的任務(wù)隊(duì)列(尚未執(zhí)行)
當(dāng)正在執(zhí)行的任務(wù)數(shù)小于最大值(默認(rèn)為64)并且,同一個(gè)域名正在請(qǐng)求的任務(wù)數(shù)小于最大值(默認(rèn)5)時(shí)错负,才會(huì)立即執(zhí)行坟瓢,否則,這個(gè)任務(wù)會(huì)被加入到 readyAsyncCalls中等待安排犹撒。
那么問題來了折联,readyAsyncCalls中的任務(wù)什么時(shí)候會(huì)被執(zhí)行?
追蹤代碼:追蹤 readyAsyncCalls 的使用代碼识颊,找到遍歷
這個(gè)隊(duì)列的地方:
繼續(xù)追蹤诚镰,找到了這個(gè) finish方法:
繼續(xù)追蹤finish在哪里調(diào)用的,找到兩處:
所以祥款,得出結(jié)論:
在一個(gè)任務(wù)(無論同步還是異步)結(jié)束之后清笨,分發(fā)器中的異步任務(wù),存在兩個(gè)隊(duì)列刃跛,一個(gè)running隊(duì)列
抠艾,一個(gè)ready隊(duì)列
,當(dāng)running隊(duì)列
的size小于最大值桨昙,并且同一個(gè)域名正在執(zhí)行的任務(wù)數(shù)小于最大值時(shí)检号,可以直接加入到running隊(duì)列,立即執(zhí)行蛙酪。 如果不滿足這條件齐苛,這個(gè)異步任務(wù)就會(huì)被加入到 ready隊(duì)列.在任意一個(gè)任務(wù)(
無論同步或是異步任務(wù)
)執(zhí)行完畢(無論成敗
)之后,就會(huì)遍歷ready隊(duì)列
桂塞,每次從ready隊(duì)列
中取出一個(gè)任務(wù)凹蜂,判斷同時(shí)執(zhí)行的異步任務(wù)數(shù)是否達(dá)到上限,并且同一主機(jī)的訪問數(shù)是否達(dá)到上限,如果都滿足玛痊,就加入到running隊(duì)列泥彤,并且立即執(zhí)行,不滿足卿啡,就停止遍歷吟吝。周而復(fù)始,直到所有的異步任務(wù)都執(zhí)行完颈娜。
文字不夠形象剑逃,畫個(gè)圖表示。
關(guān)于okhttp的分發(fā)器Dispatcher用到的線程池
同步請(qǐng)求官辽,沒有用到線程池蛹磺。
但是異步請(qǐng)求的代碼中,有這么一句同仆。
我們知道萤捆,為什么這里會(huì)用到線程池呢?
1.觀察 同步或者異步的call的實(shí)例俗批。
那么這個(gè)Call
是什么俗或?它是一個(gè)接口,它的唯一實(shí)現(xiàn)類是RealCall
,
在RealCall
中岁忘,異步請(qǐng)求的執(zhí)行方法辛慰,enqueue()
其實(shí)是交給了 分發(fā)器一個(gè)AsyncCall
對(duì)象,它繼承自NamedRunnable
可命名的Runnable
任務(wù)干像。所以帅腌,這里可以用 線程池ExecutorService
來執(zhí)行這個(gè)Runnable
.
進(jìn)一步觀察這個(gè)線程池的細(xì)節(jié):
它是一個(gè)核心線程數(shù)為0的線程池,并且使用了一個(gè)無容量的阻塞隊(duì)列作為參數(shù)麻汰。
其實(shí)也不不必自己去創(chuàng)建線程池速客,而可以直接使用Executors.newCachedThreadPool();
來創(chuàng)建,效果一樣五鲫。
線程池溺职,系統(tǒng)提供了有多種默認(rèn)實(shí)現(xiàn)
為什么okhttp偏偏選擇了這一種?
答:為了實(shí)現(xiàn)最大并發(fā)量臣镣。
詳解如下:
既然這里提到了線程池辅愿,那么就把線程池的基本機(jī)制整理一下:
線程池的構(gòu)造函數(shù)中智亮,有一個(gè)阻塞隊(duì)列參數(shù)忆某。
它有3個(gè)實(shí)現(xiàn)類:ArrayBlockingDeque
/LinkedBlockingDeque
/SynchronousQueue
是我們線程池經(jīng)常用的。
前面2個(gè)都是有容量的阔蛉,而第三個(gè)是無容量的弃舒,加入進(jìn)去,一定會(huì)失敗。而參照上面線程池的工作流程圖聋呢,如果加入失敗苗踪,就會(huì)嘗試去非核心線程執(zhí)行任務(wù)。這樣削锰,便保證了每一個(gè)提交進(jìn)來的異步任務(wù)通铲,都會(huì)立即嘗試去執(zhí)行,而不是塞入等待隊(duì)列中等待空閑線程器贩,從而確保了 異步任務(wù)的并發(fā)颅夺。
OkHttp源碼核心類之一:攔截器簡(jiǎn)述
上面講解分發(fā)器的時(shí)候,提到了 RealCall類的getResponseWithInterceptorChain()
方法蛹稍。它是一個(gè)網(wǎng)絡(luò)請(qǐng)求執(zhí)行的真正核心方法吧黄。
進(jìn)入方法:
- 新建一個(gè)攔截器List,并且放入各種攔截器對(duì)象
- 將攔截器list唆姐,交給RealInterceptorChain拗慨,進(jìn)行責(zé)任鏈模式的調(diào)用,最終得出Response.
首先解釋一下責(zé)任鏈模式
奉芦,它是21種基本設(shè)計(jì)模式中赵抢,行為模式中一種。下面的案例可以很好地解釋它:
當(dāng)一個(gè)國(guó)企要采購(gòu)一批設(shè)備的時(shí)候声功,按照上圖整個(gè)任務(wù)流程中昌讲,存在5個(gè)對(duì)象,都能對(duì)采購(gòu)流程造成影響减噪,采購(gòu)任務(wù)開始的時(shí)候短绸,是從上到下依次對(duì)采購(gòu)流程負(fù)責(zé)。而總經(jīng)理筹裕,他才不關(guān)心下面的人怎么操作醋闭,他只關(guān)心最后的結(jié)果。
正如此案例中所述朝卒,okhttp的責(zé)任鏈模式证逻,使用者也不需要關(guān)心這個(gè)請(qǐng)求到底經(jīng)歷了哪些過程,他只知道抗斤,我給了request囚企,你就要給我response,而過程中瑞眼,發(fā)生作用的各類攔截器龙宏,無需使用者知道,這樣就達(dá)成了面向?qū)ο蟪绦蜷_發(fā)
中的最少知道原則
伤疙。
而银酗,這些攔截器辆影,恰恰是okhttp的核心內(nèi)容,下篇文章將會(huì)詳細(xì)講解黍特。
結(jié)語(yǔ)
本文是okhttp
的開篇蛙讥,如果要詳細(xì)解讀okhttp
的每個(gè)細(xì)節(jié),每一篇文章將會(huì)顯得非常冗長(zhǎng)而且乏味
灭衷,所以我選了重要節(jié)點(diǎn)著重分析次慢。 就像攻城略地打天下,先占領(lǐng)據(jù)點(diǎn)
翔曲,再企圖擴(kuò)張
经备,一步一個(gè)腳印,穩(wěn)扎穩(wěn)打部默,才能長(zhǎng)遠(yuǎn)發(fā)展