手把手講解 OkHttp硬核知識(shí)點(diǎn)(1)

前言

手把手講解系列文章缚够,是我寫給各位看官,也是寫給我自己的鹦赎。
文章可能過分詳細(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陈辱、Http2Quic以及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í)行出去,流程如下圖:


image.png

OkHttp源碼核心類之一:分發(fā)器詳解

上述幽纷,提到Call類式塌,可以選擇性執(zhí)行 同步或者異步請(qǐng)求博敬,但是無論同步異步友浸,都一定會(huì)經(jīng)過一個(gè)門戶:"分發(fā)器" :
索引進(jìn)源碼(okhttp v3.10.0):

異步請(qǐng)求.png

同步請(qǐng)求.png

雖然用戶不需要直接操作分發(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ā)器之后昔榴,


image.png

可能會(huì)被加入到 Deque<AsyncCall> runningAsyncCalls 這么一個(gè)雙端隊(duì)列中,然后 executorService().execute(call);實(shí)際上是用了線程池來執(zhí)行了這個(gè)異步任務(wù)碘橘。
但是互订,請(qǐng)注意(還是剛才的enqueue方法代碼)這里有一個(gè)判斷條件 if分支 :

image.png

這個(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ì)列的地方:

image.png

繼續(xù)追蹤诚镰,找到了這個(gè) finish方法:


image.png

繼續(xù)追蹤finish在哪里調(diào)用的,找到兩處:

image.png

image.png

所以祥款,得出結(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è)圖表示。

未命名文件.jpg

關(guān)于okhttp的分發(fā)器Dispatcher用到的線程池

同步請(qǐng)求官辽,沒有用到線程池蛹磺。

image.png

但是異步請(qǐng)求的代碼中,有這么一句同仆。

image.png

我們知道萤捆,為什么這里會(huì)用到線程池呢?

1.觀察 同步或者異步的call的實(shí)例俗批。

image.png

那么這個(gè)Call是什么俗或?它是一個(gè)接口,它的唯一實(shí)現(xiàn)類是RealCall,
image.png

RealCall中岁忘,異步請(qǐng)求的執(zhí)行方法辛慰,enqueue() 其實(shí)是交給了 分發(fā)器一個(gè)AsyncCall對(duì)象,它繼承自NamedRunnable 可命名的Runnable任務(wù)干像。所以帅腌,這里可以用 線程池ExecutorService來執(zhí)行這個(gè)Runnable.

進(jìn)一步觀察這個(gè)線程池的細(xì)節(jié):

image.png

它是一個(gè)核心線程數(shù)為0的線程池,并且使用了一個(gè)無容量的阻塞隊(duì)列作為參數(shù)麻汰。
其實(shí)也不不必自己去創(chuàng)建線程池速客,而可以直接使用Executors.newCachedThreadPool(); 來創(chuàng)建,效果一樣五鲫。
線程池溺职,系統(tǒng)提供了有多種默認(rèn)實(shí)現(xiàn)
image.png

為什么okhttp偏偏選擇了這一種?

答:為了實(shí)現(xiàn)最大并發(fā)量臣镣。

詳解如下:
既然這里提到了線程池辅愿,那么就把線程池的基本機(jī)制整理一下:

線程池的工作流程圖.jpg

線程池的構(gòu)造函數(shù)中智亮,有一個(gè)阻塞隊(duì)列參數(shù)忆某。
image.png

它有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)入方法:

核心方法.png
  • 新建一個(gè)攔截器List,并且放入各種攔截器對(duì)象
  • 將攔截器list唆姐,交給RealInterceptorChain拗慨,進(jìn)行責(zé)任鏈模式的調(diào)用,最終得出Response.

首先解釋一下責(zé)任鏈模式奉芦,它是21種基本設(shè)計(jì)模式中赵抢,行為模式中一種。下面的案例可以很好地解釋它:

責(zé)任鏈模式案例

當(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ā)展


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侵蒙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子傅蹂,更是在濱河造成了極大的恐慌纷闺,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件份蝴,死亡現(xiàn)場(chǎng)離奇詭異犁功,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)婚夫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門浸卦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人案糙,你說我怎么就攤上這事限嫌。” “怎么了时捌?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵怒医,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我奢讨,道長(zhǎng)稚叹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任拿诸,我火速辦了婚禮扒袖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亩码。我一直安慰自己季率,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布蟀伸。 她就那樣靜靜地躺著蚀同,像睡著了一般缅刽。 火紅的嫁衣襯著肌膚如雪啊掏。 梳的紋絲不亂的頭發(fā)上蠢络,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音迟蜜,去河邊找鬼刹孔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娜睛,可吹牛的內(nèi)容都是我干的髓霞。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼畦戒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼方库!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起障斋,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤纵潦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后垃环,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邀层,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年遂庄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寥院。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涛目,死狀恐怖秸谢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情霹肝,我是刑警寧澤钮追,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站阿迈,受9級(jí)特大地震影響元媚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苗沧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一刊棕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧待逞,春花似錦甥角、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)震束。三九已至,卻和暖如春当犯,著一層夾襖步出監(jiān)牢的瞬間垢村,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工嚎卫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嘉栓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓拓诸,卻偏偏與公主長(zhǎng)得像侵佃,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奠支,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345