「Android 路線」| OkHttp 分發(fā)器

點(diǎn)贊關(guān)注鹉动,不再迷路浑厚,你的支持對(duì)我意義重大摹闽!

?? Hi蹄咖,我是丑丑。本文「Android 路線」| 導(dǎo)讀 —— 從零到無(wú)窮大 已收錄付鹿。這里有 Android 進(jìn)階成長(zhǎng)路線筆記 & 博客澜汤,歡迎跟著彭丑丑一起成長(zhǎng)蚜迅。(聯(lián)系方式在 GitHub)

前言

  • 網(wǎng)絡(luò)請(qǐng)求是 App 中非常重要的一個(gè)組件,而 OkHttp 作為官方和業(yè)界雙重認(rèn)可的解決方案俊抵,其學(xué)習(xí)價(jià)值不必多言谁不;
  • 在這篇文章里,我將分析 OkHttp 分發(fā)器 & 攔截器 的實(shí)現(xiàn)原理徽诲。如果能幫上忙刹帕,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要谎替。

目錄


1. 前置知識(shí)

Editting...


2. OkHttp 簡(jiǎn)介

OkHttp 是 Square 開(kāi)源的網(wǎng)絡(luò)請(qǐng)求框架偷溺,自從 Android 4.4 移除 HttpURLConnection 后,逐漸演變成 Android 端最主流的網(wǎng)絡(luò)請(qǐng)求框架钱贯。

2.1 優(yōu)點(diǎn)

  • 1挫掏、支持 Http1、Http2秩命、Quic 以及 WebSocket 等多種應(yīng)用層協(xié)議尉共;
  • 2、連接池復(fù)用底層 TCP 連接(Socket)弃锐,降低了請(qǐng)求時(shí)延袄友;
  • 3、無(wú)縫支持 GZIP 減少數(shù)據(jù)流量拿愧;
  • 4杠河、緩存響應(yīng)數(shù)據(jù),減少了重復(fù)網(wǎng)絡(luò)請(qǐng)求浇辜;
  • 5、自動(dòng)失敗重試唾戚、自動(dòng)重定向柳洋。

2.2 使用流程

一次 OkHttp 請(qǐng)求過(guò)程最少需要用到 OkHttpClient、Request叹坦、Call熊镣、Response,例如:

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
    .url(url)
    .build();

同步請(qǐng)求
Call call = client.newCall(request);
獲得響應(yīng)
Response response = call.execute();

異步請(qǐng)求
Call call = client.newCall(request);
call.enqueue(new Callback(){

    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, Response response) {
        獲得響應(yīng)
    }

});

需要注意的是募书,Call 是一個(gè)接口绪囱,OkHttpClient#newCall(...)返回的其實(shí)是它的實(shí)現(xiàn)類(lèi) RealCall:

OkHttpClient.java

public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false);
}

RealCall.java

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
}

3. Dispatcher 分發(fā)器

Call#execute() & Call#enqueue(...) 分別表示調(diào)用同步請(qǐng)求和異步請(qǐng)求,客戶端對(duì)請(qǐng)求任務(wù)的調(diào)度過(guò)程是不感知的莹捡。這是因?yàn)?OkHttp 內(nèi)部的 「Dispatcher 分發(fā)器」 封裝了整個(gè)請(qǐng)求任務(wù)的調(diào)度過(guò)程鬼吵,這一節(jié)我們來(lái)具體分析下。

3.1 自定義 Dispatcher 分發(fā)器

Dispatcher 主要成員變量如下:

Dispatcher.java

異步請(qǐng)求最大并發(fā)數(shù)
private int maxRequests = 64;

同一域名請(qǐng)求最大并發(fā)數(shù)
private int maxRequestsPerHost = 5;

閑置任務(wù)(沒(méi)有請(qǐng)求時(shí)執(zhí)行)
private Runnable idleCallback;

線程池
private ExecutorService executorService;

異步請(qǐng)求等待隊(duì)列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();

異步請(qǐng)求執(zhí)行隊(duì)列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();

同步請(qǐng)求執(zhí)行隊(duì)列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque();

其中篮赢,以下幾個(gè)變量是允許自定義的:

變量 描述 默認(rèn)值
maxRequests 異步請(qǐng)求最大并發(fā)數(shù) 64
maxRequestsPerHost 同一域名請(qǐng)求最大并發(fā)數(shù) 5
idleCallback 閑置任務(wù) null
executorService 線程池 無(wú)等待齿椅,最大并發(fā)

提示: 限制請(qǐng)求數(shù)是為了避免客戶端和服務(wù)器的負(fù)載過(guò)大(Linux 一切皆文件琉挖,Socket 占用文件文件句柄,打開(kāi)文件數(shù)是有限制的)涣脚,至于為什么默認(rèn)值為 64 和 5示辈,據(jù)說(shuō) OkHttp 是參考了主流瀏覽器得出的經(jīng)驗(yàn)值,未查及論據(jù)遣蚀。

自定義的 Dispatcher 對(duì)象通過(guò) OkHttpClient.Builder 構(gòu)建者設(shè)置:

OkHttpClient.java

public OkHttpClient.Builder dispatcher(Dispatcher dispatcher) {
    if (dispatcher == null) {
        throw new IllegalArgumentException("dispatcher == null");
    } else {
        this.dispatcher = dispatcher;
        return this;
    }
}

3.2 同步請(qǐng)求

這一節(jié)我們來(lái)分析 OkHttp 同步請(qǐng)求的執(zhí)行過(guò)程:

RealCall.java

已簡(jiǎn)化
public Response execute() throws IOException {
    1矾麻、禁止重復(fù)執(zhí)行
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }
        this.executed = true;
    }

    2、記錄
    this.client.dispatcher().executed(this);
    3芭梯、執(zhí)行請(qǐng)求射富,阻塞等待響應(yīng)返回
    Response result = this.getResponseWithInterceptorChain();
    4、移除記錄
    this.client.dispatcher().finished(this);

    return result;
}

Dispatcher.java

記錄到 runningSyncCalls 中
synchronized void executed(RealCall call) {
    this.runningSyncCalls.add(call);
}

從 runningSyncCalls 中移除
void finished(RealCall call) {
    內(nèi)部邏輯見(jiàn) 第 3.3 節(jié)
    finished(runningSyncCalls, call);
}

同步請(qǐng)求是在當(dāng)前線程阻塞執(zhí)行粥帚,所以不需要 Dispatcher 調(diào)度任務(wù)胰耗,Dispatcher 內(nèi)部也僅僅是通過(guò) runningSyncCalls 記錄同步請(qǐng)求。

同時(shí)可以看到芒涡,RealCall#getResponseWithInterceptorChain() 是真正執(zhí)行請(qǐng)求的地方柴灯,我一并在 「Android 路線」| OkHttp 攔截器 中分析。

3.3 異步請(qǐng)求

這一節(jié)我們來(lái)分析 OkHttp 異步請(qǐng)求的執(zhí)行過(guò)程:

RealCall.java

已簡(jiǎn)化
public void enqueue(Callback responseCallback) {
    1费尽、禁止重復(fù)執(zhí)行
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }
        this.executed = true;
    }
    2赠群、調(diào)用分發(fā)器
    this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}

OkHttp 異步請(qǐng)求就需要 Dispatcher 登場(chǎng)了,我們來(lái)看看:

Dispatcher.java

void enqueue(AsyncCall call) {
    synchronized (this) {
        2.1 加入 readyAsyncCalls
        readyAsyncCalls.add(call);

        if (!call.get().forWebSocket) {
            2.2 復(fù)用同一個(gè)計(jì)數(shù)器(記錄同一域名異步請(qǐng)求數(shù))
            AsyncCall existingCall = findExistingCallWithHost(call.host());
            if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
        }
    }
    2.3 觸發(fā)或執(zhí)行請(qǐng)求
    promoteAndExecute();
}

主要的邏輯應(yīng)該在 Dispatcher#promoteAndExecute() 中:

2.3 觸發(fā)或執(zhí)行請(qǐng)求
private boolean promoteAndExecute() {
    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
        1旱幼、遍歷 readyAsyncCalls 列表
        for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
            AsyncCall asyncCall = i.next();

            2查描、判斷全部異步請(qǐng)求并發(fā)數(shù)是否超過(guò)限制
            if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
            3、判斷同一域名異步請(qǐng)求并發(fā)數(shù)是否超過(guò)限制
            if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.

            4柏卤、符合執(zhí)行條件
            i.remove();
            4.1冬三、同一域名異步請(qǐng)求數(shù)加一
            asyncCall.callsPerHost().incrementAndGet();
            4.2 可執(zhí)行任務(wù)列表
            executableCalls.add(asyncCall);
            4.3 執(zhí)行中任務(wù)列表
            runningAsyncCalls.add(asyncCall);
        }
        isRunning = runningCallsCount() > 0;
    }

    5 處理可執(zhí)行任務(wù)列表
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
        AsyncCall asyncCall = executableCalls.get(i);
        執(zhí)行請(qǐng)求
        asyncCall.executeOn(executorService());
    }

    6 返回是否正在執(zhí)行請(qǐng)求(若無(wú)請(qǐng)求,則執(zhí)行閑置任務(wù) idleCallback)
    return isRunning;
}

可以看到缘缚,異步請(qǐng)求是存在限制的勾笆,只有 「全部異步請(qǐng)求并發(fā)數(shù)不超過(guò)限制」 & 「同一域名異步請(qǐng)求并發(fā)數(shù)不超過(guò)限制」,才允許執(zhí)行桥滨,否則會(huì)停留在 readyAsyncCalls 中等待窝爪。

那么,誰(shuí)來(lái)拯救在 readyAsyncCalls 中等待的請(qǐng)求呢齐媒?其實(shí)就是看什么時(shí)候會(huì)觸發(fā)promoteAndExecute()方法了蒲每,除了 setMaxRequests(int)、setMaxRequestsPerHost(int)外喻括,在異步請(qǐng)求完成后邀杏,讓出 “空閑名額” 也會(huì)觸發(fā)。

AsyncCall.java

5 處理可執(zhí)行任務(wù)列表
void executeOn(ExecutorService executorService) {
    boolean success = false;
    try {
        5.1 線程池執(zhí)行
        executorService.execute(this);
        success = true;
    } catch (RejectedExecutionException e) {
        5.2 線程池拒絕執(zhí)行
        InterruptedIOException ioException = new InterruptedIOException("executor rejected");
        ioException.initCause(e);
        eventListener.callFailed(RealCall.this, ioException);
        responseCallback.onFailure(RealCall.this, ioException);
    } finally {
        5.3 finish
        if (!success) {
            client.dispatcher().finished(this); // This call is no longer running!
        }
    }
}

5.1 線程池執(zhí)行(已簡(jiǎn)化)
protected void execute() {
    try {
        5.1.1 執(zhí)行請(qǐng)求双妨,阻塞等待響應(yīng)返回
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
            5.1.2 請(qǐng)求取消
            responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
            5.1.3 請(qǐng)求成功
            responseCallback.onResponse(RealCall.this, response);
        }
    } catch (IOException e) {
        5.1.4 請(qǐng)求失敗
        responseCallback.onFailure(RealCall.this, e);
    } finally {
        5.1.5 finish
        client.dispatcher().finished(this);
    }
}

這段代碼不算復(fù)雜淮阐,AsyncCall 是 Runnable 的子類(lèi)叮阅,Runnable#run()最終會(huì)走到AsyncCall#execute(),主要分為三步:

  • 調(diào)用getResponseWithInterceptorChain()來(lái)獲得請(qǐng)求響應(yīng)泣特,見(jiàn) 「Android 路線」| OkHttp 攔截器浩姥;
  • 調(diào)用 responseCallback 回調(diào);
  • 調(diào)用 Dispatcher#finished()状您。

Dispatcher#finished() 我們?cè)?第 3.2 節(jié) 同步請(qǐng)求里見(jiàn)過(guò)勒叠,現(xiàn)在我們來(lái)分析下:

Dispatcher.java

5.1.5 finish 或 5.3 finish
private <T> void finished(Deque<T> calls, T call) {
    Runnable idleCallback;
    synchronized (this) {
        1、從移除請(qǐng)求執(zhí)行列表中移除
        if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
        idleCallback = this.idleCallback;
    }

    2膏孟、觸發(fā)或執(zhí)行請(qǐng)求
    boolean isRunning = promoteAndExecute();

    3眯分、若無(wú)請(qǐng)求,則執(zhí)行閑置任務(wù) idleCallback
    if (!isRunning && idleCallback != null) {
        idleCallback.run();
    }
}

4. 分發(fā)器線程池

為什么不能使用ArrayBlockingQueue()
為什么不能使用LinkedBlockingQueue
為什么使用SynchronousQueue


5. 總結(jié)

看到這里柒桑,我們先來(lái)總結(jié)這篇文章的內(nèi)容以及遇到的疑問(wèn):

  • 1弊决、同步請(qǐng)求不需要 Dispatcher 任務(wù)調(diào)度,Dispatcher 只做記錄魁淳;
  • 2 飘诗、異步請(qǐng)求有限制,被限制的請(qǐng)求會(huì)在 ready 隊(duì)列中等待界逛,直到有請(qǐng)求完成讓出 “空閑名額” 才會(huì)觸發(fā)執(zhí)行昆稿;

在前面關(guān)于同步請(qǐng)求和異步請(qǐng)求的分析中,我們都提到了 RealCall#getResponseWithInterceptorChain() 是真正執(zhí)行請(qǐng)求的地方息拜,我在 「Android 路線」| OkHttp 攔截器 里討論溉潭。


創(chuàng)作不易,你的「三連」是丑丑最大的動(dòng)力少欺,我們下次見(jiàn)喳瓣!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狈茉,隨后出現(xiàn)的幾起案子划纽,更是在濱河造成了極大的恐慌被芳,老刑警劉巖缸濒,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狮惜,死亡現(xiàn)場(chǎng)離奇詭異上荡,居然都是意外死亡抗碰,警方通過(guò)查閱死者的電腦和手機(jī)价脾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)随珠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)羽莺,“玉大人实昨,你說(shuō)我怎么就攤上這事⊙喂蹋” “怎么了荒给?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵丈挟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我志电,道長(zhǎng)曙咽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任挑辆,我火速辦了婚禮例朱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鱼蝉。我一直安慰自己洒嗤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布魁亦。 她就那樣靜靜地躺著渔隶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洁奈。 梳的紋絲不亂的頭發(fā)上间唉,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音睬魂,去河邊找鬼终吼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛氯哮,可吹牛的內(nèi)容都是我干的际跪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼喉钢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼姆打!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起肠虽,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤幔戏,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后税课,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體闲延,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年韩玩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了垒玲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡找颓,死狀恐怖合愈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤佛析,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布益老,位于F島的核電站,受9級(jí)特大地震影響寸莫,放射性物質(zhì)發(fā)生泄漏捺萌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一储狭、第九天 我趴在偏房一處隱蔽的房頂上張望互婿。 院中可真熱鬧,春花似錦辽狈、人聲如沸慈参。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)驮配。三九已至,卻和暖如春着茸,著一層夾襖步出監(jiān)牢的瞬間壮锻,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工涮阔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留猜绣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓敬特,卻偏偏與公主長(zhǎng)得像掰邢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伟阔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 這篇文章主要講 Android 網(wǎng)絡(luò)請(qǐng)求時(shí)所使用到的各個(gè)請(qǐng)求庫(kù)的關(guān)系辣之,以及 OkHttp3 的介紹。(如理解有誤皱炉,...
    小莊bb閱讀 1,145評(píng)論 0 4
  • 概述 從OkHttp問(wèn)世以來(lái)怀估,度娘,google上關(guān)于OkHttp的講解說(shuō)明數(shù)不勝數(shù)合搅,各種解讀思想不盡相同多搀,一千個(gè)...
    夜貓少年閱讀 5,729評(píng)論 9 14
  • 簡(jiǎn)介 OkHttp是當(dāng)下Android使用最頻繁的網(wǎng)絡(luò)請(qǐng)求框架,由Square公司開(kāi)源灾部。Google在Androi...
    Thisislife閱讀 1,755評(píng)論 1 1
  • 前言 閱讀過(guò)上一篇對(duì)網(wǎng)絡(luò)編程的概述一文后酗昼,應(yīng)該對(duì)網(wǎng)絡(luò)編程有一個(gè)大體的概念了。從本文開(kāi)始梳猪,將會(huì)開(kāi)始對(duì)OkHttp的源...
    yjy239閱讀 1,050評(píng)論 0 3
  • 漸變的面目拼圖要我怎么拼? 我是疲乏了還是投降了匿沛? 不是不允許自己墜落扫责, 我沒(méi)有滴水不進(jìn)的保護(hù)膜。 就是害怕變得面...
    悶熱當(dāng)乘涼閱讀 4,234評(píng)論 0 13