OkHttp3源碼解析(一)分發(fā)器Dispatcher原理分析

OkHttp3源碼解析(一)分發(fā)器Dispatcher原理分析
OkHttp3源碼解析(二)五大攔截器原理分析

OkHttp 3.10.0版本,最新OkHttp為:4.0.1邏輯與3版本并沒有太大變化,但是改為kotlin實(shí)現(xiàn)烫止。

OkHttp介紹

OkHttp是當(dāng)下Android使用最頻繁的網(wǎng)絡(luò)請求框架几于,由Square公司開源话侧。Google在Android4.4以后開始將源碼中的HttpURLConnection底層實(shí)現(xiàn)替換為OKHttp,同時(shí)現(xiàn)在流行的Retrofit框架底層同樣是使用OKHttp的益老。

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

  • 支持Spdy彪蓬、Http1.X、Http2捺萌、Quic以及WebSocket
  • 連接池復(fù)用底層TCP(Socket)档冬,減少請求延時(shí)
  • 無縫的支持GZIP減少數(shù)據(jù)流量
  • 緩存響應(yīng)數(shù)據(jù)減少重復(fù)的網(wǎng)絡(luò)請求
  • 請求失敗自動(dòng)重試主機(jī)的其他ip,自動(dòng)重定向
  • …….
異步get請求
String url = "http://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request = new Request.Builder()
        .url(url)
        .get()//默認(rèn)就是GET請求桃纯,可以不寫
        .build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.d(TAG, "onFailure: ");
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        Log.d(TAG, "onResponse: " + response.body().string());
    }
});
異步Post請求
OkHttpClient okHttpClient = new OkHttpClient();
RequestBody requestBody = new FormBody.Builder()
        .add("search", "Jurassic Park")
        .build();
Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(requestBody)
        .build();
okHttpClient.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        Log.d(TAG, "onFailure: " + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        Log.d(TAG, response.protocol() + " " +response.code() + " " + response.message());
        Headers headers = response.headers();
        for (int i = 0; i < headers.size(); i++) {
            Log.d(TAG, headers.name(i) + ":" + headers.value(i));
        }
        Log.d(TAG, "onResponse: " + response.body().string());
    }
});

1酷誓、OkHttp的調(diào)用流程

image.png

OkHttp請求過程中最少只需要接觸OkHttpClient、Request态坦、Call盐数、Response,但是框架內(nèi)部進(jìn)行大量的邏輯處理伞梯。

所有的邏輯大部分集中在攔截器中玫氢,但是在進(jìn)入攔截器之前還需要依靠分發(fā)器來調(diào)配請求任務(wù)。

  • 分發(fā)器:內(nèi)部維護(hù)隊(duì)列與線程池谜诫,完成請求調(diào)配漾峡;

  • 攔截器:五大默認(rèn)攔截器完成整個(gè)請求過程。

2猜绣、分發(fā)器:異步請求的工作流程

Call call = client.newCall(request);
Response response = call.enqueue();

final class RealCall implements Call

=> RealCall#enqueue
=> client.dispatcher().enqueue(new AsyncCall(responseCallback));
=> Dispatcher#enqueue

當(dāng)我們調(diào)用client.dispatcher().enqueue(new AsyncCall(responseCallback));的時(shí)候會(huì)進(jìn)入Dispatcher的enqueue方法灰殴,

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
    } else {
        readyAsyncCalls.add(call);
    }
}
image.png

一開始會(huì)傳一個(gè)AsyncCall到dispatcher中,在dispatcher的enqueue里,AsyncCall可能會(huì)加入到正在運(yùn)行的runningAsyncCalls隊(duì)列或者加入到正在等待的readyAsyncCalls隊(duì)列牺陶,如果加入到runningAsyncCalls隊(duì)列的話伟阔,會(huì)被進(jìn)一步提交到ThreadPool線程池中,因此AsyncCall肯定是一個(gè)實(shí)現(xiàn)了Runnable接口的“任務(wù)”掰伸。當(dāng)這個(gè)任務(wù)在線程池中執(zhí)行完畢的時(shí)候皱炉,會(huì)調(diào)用dispatcher里的finished方法,在finished中又會(huì)調(diào)用到dispatcher里的promoteCalls方法狮鸭,去遍歷readyAsyncCalls隊(duì)列合搅,找出滿足條件的任務(wù),加入到runningAsyncCalls隊(duì)列里歧蕉,又開始新一輪的執(zhí)行灾部。這就是分發(fā)器里整個(gè)異步請求的工作流程。

那么問題來了:

  • 1惯退、分發(fā)器dispatcher如何決定將請求放入runningAsyncCalls還是readyAsyncCalls赌髓?
  • 2、從readyAsyncCalls移動(dòng)到runningAsyncCalls的條件是什么催跪?
  • 3锁蠕、分發(fā)器dispatcher線程池是怎么工作的?

3懊蒸、源碼分析

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
    } else {
        readyAsyncCalls.add(call);
    }
}

當(dāng)正在請求的任務(wù)數(shù)量runningAsyncCalls小于maxRequests(異步請求同時(shí)存在的最大請求默認(rèn)64個(gè))荣倾,這個(gè)maxRequests數(shù)量也是可以通過setMaxRequests方法來設(shè)置的,但最小不能小于1個(gè)骑丸,不然會(huì)報(bào)異常舌仍。另外一個(gè)限制是:異步請求同一域名同時(shí)存在的最大請求runningCallsForHost不能大于maxRequestsPerHost(默認(rèn)是5個(gè)),當(dāng)然maxRequestsPerHost也是能通過setMaxRequestsPerHost來自己重新設(shè)置的通危。如果滿足這兩個(gè)條件抡笼,就會(huì)把這個(gè)AsyncCall也就是Runnable加入到runningAsyncCalls隊(duì)列中,同時(shí)加入到executorService線程池中黄鳍,并且調(diào)用execute開始執(zhí)行這個(gè)任務(wù)。

public synchronized ExecutorService executorService() {
    if (executorService == null) {
        executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher",
                false));
    }
    return executorService;
}

進(jìn)入到這個(gè)線程池平匈,首先核心線程為0框沟,表示線程池不會(huì)一直為我們緩存線程,線程池中所有線程都是在60s內(nèi)沒有工作就會(huì)被回收增炭。而最大線程Integer.MAX_VALUE與等待隊(duì)列SynchronousQueue的組合能夠得到最大的吞吐量忍燥。即當(dāng)需要線程池執(zhí)行任務(wù)時(shí),如果不存在空閑線程不需要等待隙姿,馬上新建線程執(zhí)行任務(wù)梅垄!等待隊(duì)列的不同指定了線程池的不同排隊(duì)機(jī)制。一般來說输玷,等待隊(duì)列BlockingQueue有:ArrayBlockingQueue队丝、LinkedBlockingQueueSynchronousQueue靡馁。那么為什么要使用SynchronousQueue : 無容量的隊(duì)列。其實(shí)机久,使用此隊(duì)列意味著希望獲得最大并發(fā)量臭墨。因?yàn)闊o論如何,向線程池提交任務(wù)膘盖,往隊(duì)列提交任務(wù)都會(huì)失敗胧弛。而失敗后如果沒有空閑的非核心線程,就會(huì)檢查如果當(dāng)前線程池中的線程數(shù)未達(dá)到最大線程侠畔,則會(huì)新建線程執(zhí)行新提交的任務(wù)结缚。完全沒有任何等待,唯一制約它的就是最大線程數(shù)的個(gè)數(shù)软棺。因此一般配合Integer.MAX_VALUE就實(shí)現(xiàn)了真正的無等待红竭。

回到之前的代碼,如果不滿足码党,加入到等待隊(duì)列readyAsyncCalls德崭。那么這個(gè)Runnable真正在哪里執(zhí)行它的run方法呢?

final class AsyncCall extends NamedRunnable {
public abstract class NamedRunnable implements Runnable {
    protected final String name;

    public NamedRunnable(String format, Object... args) {
        this.name = Util.format(format, args);
    }

    @Override
    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(name);
        try {
            execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }
    }

    protected abstract void execute();
}

我們看到AsyncCall是繼承自NamedRunnable揖盘,NamedRunnable又實(shí)現(xiàn)了Runnable接口眉厨,在NamedRunnable的run方法里調(diào)用了execute方法,execute是個(gè)抽象方法兽狭,所以它真正實(shí)現(xiàn)的地方就是在AsyncCall的execute方法里

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    AsyncCall(Callback responseCallback) {
        super("OkHttp %s", redactedUrl());
        this.responseCallback = responseCallback;
    }

    String host() {
        return originalRequest.url().host();
    }

    Request request() {
        return originalRequest;
    }

    RealCall get() {
        return RealCall.this;
    }

    @Override
    protected void execute() {
        boolean signalledCallback = false;
        try {
            Response response = getResponseWithInterceptorChain();
            if (retryAndFollowUpInterceptor.isCanceled()) {
                signalledCallback = true;
                responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
            } else {
                signalledCallback = true;
                responseCallback.onResponse(RealCall.this, response);
            }
        } catch (IOException e) {
            if (signalledCallback) {
                // Do not signal the callback twice!
                Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
            } else {
                eventListener.callFailed(RealCall.this, e);
                responseCallback.onFailure(RealCall.this, e);
            }
        } finally {
            client.dispatcher().finished(this);
        }
    }
}

所以憾股,我們先不去關(guān)心這個(gè)任務(wù)AsyncCall到底是怎么執(zhí)行的。我們看到最后也就是任務(wù)跑完了箕慧,不管成功或者失敗服球,一定會(huì)去執(zhí)行finally里的代碼塊client.dispatcher().finished(this)表示這個(gè)任務(wù)結(jié)束了。所以我們先去看看dispatcher里的finished到底是怎么執(zhí)行的颠焦。

void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
        if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
        if (promoteCalls) promoteCalls();
        runningCallsCount = runningCallsCount();
        idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
        idleCallback.run();
    }
}

一開始把call從calls里(也就是傳進(jìn)來的runningAsyncCalls)移除斩熊。然后執(zhí)行promoteCalls方法

private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall call = i.next();

        if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            runningAsyncCalls.add(call);
            executorService().execute(call);
        }

        if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
}

一開始也是判斷maxRequests和maxRequestsPerHost,然后遍歷readyAsyncCalls隊(duì)列伐庭,把任務(wù)從readyAsyncCalls里取出加入到runningAsyncCalls里粉渠。到這里,分發(fā)器里整個(gè)異步請求的工作流程源碼分析完成了圾另。同步請求就不展開講了霸株,很簡單,收到任務(wù)時(shí)加入runningSyncCalls隊(duì)列集乔,執(zhí)行完畢從runningSyncCalls隊(duì)列移除去件。

最后,總結(jié)一下:

  • Q1: 如何決定將請求放入ready還是running?

    如果當(dāng)前正在請求數(shù)不小于64放入ready尤溜;如果小于64倔叼,但是已經(jīng)存在同一域名主機(jī)的請求5個(gè)放入ready!

  • Q2: 從running移動(dòng)ready的條件是什么靴跛?

    每個(gè)請求執(zhí)行完成就會(huì)從running移除缀雳,同時(shí)進(jìn)行第一步相同邏輯的判斷,決定是否移動(dòng)梢睛!

  • Q3: 分發(fā)器線程池的工作行為肥印?

    無等待,最大并發(fā)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绝葡,一起剝皮案震驚了整個(gè)濱河市深碱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌藏畅,老刑警劉巖敷硅,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異愉阎,居然都是意外死亡绞蹦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門榜旦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幽七,“玉大人,你說我怎么就攤上這事溅呢≡杪牛” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵咐旧,是天一觀的道長驶鹉。 經(jīng)常有香客問我,道長铣墨,這世上最難降的妖魔是什么室埋? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮伊约,結(jié)果婚禮上词顾,老公的妹妹穿的比我還像新娘。我一直安慰自己碱妆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布昔驱。 她就那樣靜靜地躺著疹尾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纳本,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天窍蓝,我揣著相機(jī)與錄音,去河邊找鬼繁成。 笑死吓笙,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的巾腕。 我是一名探鬼主播面睛,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尊搬!你這毒婦竟也來了叁鉴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤佛寿,失蹤者是張志新(化名)和其女友劉穎幌墓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冀泻,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡常侣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弹渔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胳施。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖捞附,靈堂內(nèi)的尸體忽然破棺而出巾乳,到底是詐尸還是另有隱情,我是刑警寧澤鸟召,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布胆绊,位于F島的核電站,受9級特大地震影響欧募,放射性物質(zhì)發(fā)生泄漏压状。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一跟继、第九天 我趴在偏房一處隱蔽的房頂上張望种冬。 院中可真熱鬧,春花似錦舔糖、人聲如沸娱两。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽十兢。三九已至趣竣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旱物,已是汗流浹背遥缕。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宵呛,地道東北人单匣。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像宝穗,于是被迫代替她去往敵國和親户秤。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344