OkHttp解析

一贴见、整體思路

從使用方法出發(fā)烘苹,首先是怎么使用,其次是我們使用的功能在內(nèi)部是如何實現(xiàn)的片部,實現(xiàn)方案上有什么技巧镣衡,有什么范式。全文基本上是對 OkHttp 源碼的一個分析與導(dǎo)讀档悠,非常建議大家下載 OkHttp 源碼之后廊鸥,跟著本文,過一遍源碼辖所。對于技巧和范式惰说,由于目前我的功力還不到位,分析內(nèi)容沒多少缘回,歡迎大家和我一起討論吆视。

首先放一張完整流程圖(看不懂沒關(guān)系典挑,慢慢往后看):

image

二、基本用例

來自OkHttp 官方網(wǎng)站啦吧。

2.1.創(chuàng)建 OkHttpClient 對象

OkHttpClient client = new OkHttpClient();

咦您觉,怎么不見 builder?莫急丰滑,且看其構(gòu)造函數(shù):

public OkHttpClient() {
  this(new Builder());
}

原來是方便我們使用顾犹,提供了一個“快捷操作”,全部使用了默認的配置褒墨。OkHttpClient.Builder類成員很多炫刷,后面我們再慢慢分析,這里先暫時略過:

public Builder() {
  dispatcher = new Dispatcher();
  protocols = DEFAULT_PROTOCOLS;
  connectionSpecs = DEFAULT_CONNECTION_SPECS;
  proxySelector = ProxySelector.getDefault();
  cookieJar = CookieJar.NO_COOKIES;
  socketFactory = SocketFactory.getDefault();
  hostnameVerifier = OkHostnameVerifier.INSTANCE;
  certificatePinner = CertificatePinner.DEFAULT;
  proxyAuthenticator = Authenticator.NONE;
  authenticator = Authenticator.NONE;
  connectionPool = new ConnectionPool();
  dns = Dns.SYSTEM;
  followSslRedirects = true;
  followRedirects = true;
  retryOnConnectionFailure = true;
  connectTimeout = 10_000;
  readTimeout = 10_000;
  writeTimeout = 10_000;
}

2.2.發(fā)起 HTTP 請求

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

OkHttpClient實現(xiàn)了Call.Factory郁妈,負責(zé)根據(jù)請求創(chuàng)建新的Call浑玛。

那我們現(xiàn)在就來看看它是如何創(chuàng)建 Call 的:

/**
  * Prepares the {@code request} to be executed at some point in the future.
  */
@Override public Call newCall(Request request) {
  return new RealCall(this, request);
}

如此看來功勞全在RealCall類了,下面我們一邊分析同步網(wǎng)絡(luò)請求的過程噩咪,一邊了解RealCall的具體內(nèi)容顾彰。

2.2.1.同步網(wǎng)絡(luò)請求

我們首先看RealCall#execute

@Override public Response execute() throws IOException {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");  // (1)
    executed = true;
  }
  try {
    client.dispatcher().executed(this);                                 // (2)
    Response result = getResponseWithInterceptorChain();                // (3)
    if (result == null) throw new IOException("Canceled");
    return result;
  } finally {
    client.dispatcher().finished(this);                                 // (4)
  }
}

這里我們做了 4 件事:

  1. 檢查這個 call 是否已經(jīng)被執(zhí)行了,每個 call 只能被執(zhí)行一次胃碾,如果想要一個完全一樣的 call涨享,可以利用call#clone方法進行克隆。
  2. 利用client.dispatcher().executed(this)來進行實際執(zhí)行dispatcher是剛才看到的OkHttpClient.Builder的成員之一仆百,它的文檔說自己是異步 HTTP 請求的執(zhí)行策略厕隧,現(xiàn)在看來,同步請求它也有摻和俄周。
  3. 調(diào)用getResponseWithInterceptorChain()函數(shù)獲取 HTTP 返回結(jié)果吁讨,從函數(shù)名可以看出,這一步還會進行一系列“攔截”操作峦朗。
  4. 最后還要通知dispatcher自己已經(jīng)執(zhí)行完畢建丧。

dispatcher 這里我們不過度關(guān)注,在同步執(zhí)行的流程中波势,涉及到 dispatcher 的內(nèi)容只不過是告知它我們的執(zhí)行狀態(tài)翎朱,比如開始執(zhí)行了(調(diào)用executed),比如執(zhí)行完畢了(調(diào)用finished)艰亮,在異步執(zhí)行流程中它會有更多的參與闭翩。

真正發(fā)出網(wǎng)絡(luò)請求,解析返回結(jié)果的迄埃,還是getResponseWithInterceptorChain

private Response getResponseWithInterceptorChain() throws IOException {
  // Build a full stack of interceptors.
  List<Interceptor> interceptors = new ArrayList<>();
  interceptors.addAll(client.interceptors());
  interceptors.add(retryAndFollowUpInterceptor);
  interceptors.add(new BridgeInterceptor(client.cookieJar()));
  interceptors.add(new CacheInterceptor(client.internalCache()));
  interceptors.add(new ConnectInterceptor(client));
  if (!retryAndFollowUpInterceptor.isForWebSocket()) {
    interceptors.addAll(client.networkInterceptors());
  }
  interceptors.add(new CallServerInterceptor(
      retryAndFollowUpInterceptor.isForWebSocket()));

  Interceptor.Chain chain = new RealInterceptorChain(
      interceptors, null, null, null, 0, originalRequest);
  return chain.proceed(originalRequest);
}

OkHttp 開發(fā)者之一介紹 OkHttp 的文章里面疗韵,作者講到:

the whole thing is just a stack of built-in interceptors.

可見Interceptor是 OkHttp 最核心的一個東西,不要誤以為它只負責(zé)攔截請求進行一些額外的處理(例如 cookie)侄非,實際上它把實際的網(wǎng)絡(luò)請求蕉汪、緩存流译、透明壓縮等功能都統(tǒng)一了起來,每一個功能都只是一個Interceptor者疤,它們再連接成一個Interceptor.Chain福澡,環(huán)環(huán)相扣,最終圓滿完成一次網(wǎng)絡(luò)請求驹马。

getResponseWithInterceptorChain函數(shù)我們可以看到Interceptor.Chain的分布依次是:

image
  1. 在配置OkHttpClient時設(shè)置的interceptors革砸;
  2. 負責(zé)失敗重試以及重定向的RetryAndFollowUpInterceptor
  3. 負責(zé)把用戶構(gòu)造的請求轉(zhuǎn)換為發(fā)送到服務(wù)器的請求糯累、把服務(wù)器返回的響應(yīng)轉(zhuǎn)換為用戶友好的響應(yīng)的BridgeInterceptor算利;
  4. 負責(zé)讀取緩存直接返回、更新緩存的CacheInterceptor泳姐;
  5. 負責(zé)和服務(wù)器建立連接的ConnectInterceptor效拭;
  6. 配置OkHttpClient時設(shè)置的networkInterceptors
  7. 負責(zé)向服務(wù)器發(fā)送請求數(shù)據(jù)胖秒、從服務(wù)器讀取響應(yīng)數(shù)據(jù)CallServerInterceptor缎患。

在這里,位置決定了功能阎肝,最后一個 Interceptor 一定是負責(zé)和服務(wù)器實際通訊的挤渔,重定向、緩存等一定是在實際通訊之前的风题。

責(zé)任鏈模式在這個Interceptor鏈條中得到了很好的實踐蚂蕴。

它包含了一些命令對象和一系列的處理對象,每一個處理對象決定它能處理哪些命令對象俯邓,它也知道如何將它不能處理的命令對象傳遞給該鏈中的下一個處理對象。該模式還描述了往該處理鏈的末尾添加新的處理對象的方法熔号。

對于把Request變成Response這件事來說稽鞭,每個Interceptor都可能完成這件事,所以我們循著鏈條讓每個Interceptor自行決定能否完成任務(wù)以及怎么完成任務(wù)(自力更生或者交給下一個Interceptor)引镊。這樣一來朦蕴,完成網(wǎng)絡(luò)請求這件事就徹底從RealCall類中剝離了出來,簡化了各自的責(zé)任和邏輯弟头。兩個字:優(yōu)雅吩抓!

責(zé)任鏈模式在安卓系統(tǒng)中也有比較典型的實踐,例如 view 系統(tǒng)對點擊事件(TouchEvent)的處理赴恨。

回到 OkHttp疹娶,在這里我們先簡單分析一下ConnectInterceptorCallServerInterceptor,看看 OkHttp 是怎么進行和服務(wù)器的實際通信的伦连。

2.2.1.1.建立連接:ConnectInterceptor
@Override public Response intercept(Chain chain) throws IOException {
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Request request = realChain.request();
  StreamAllocation streamAllocation = realChain.streamAllocation();

  // We need the network to satisfy this request. Possibly for validating a conditional GET.
  boolean doExtensiveHealthChecks = !request.method().equals("GET");
  HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
  RealConnection connection = streamAllocation.connection();

  return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

實際上建立連接就是創(chuàng)建了一個HttpCodec對象雨饺,它將在后面的步驟中被使用钳垮,那它又是何方神圣呢?它是對 HTTP 協(xié)議操作的抽象额港,有兩個實現(xiàn):Http1CodecHttp2Codec饺窿,顧名思義,它們分別對應(yīng) HTTP/1.1 和 HTTP/2 版本的實現(xiàn)移斩。

Http1Codec中肚医,它利用OkioSocket的讀寫操作進行封裝,Okio 以后有機會再進行分析向瓷,現(xiàn)在讓我們對它們保持一個簡單地認識:它對java.iojava.nio進行了封裝肠套,讓我們更便捷高效的進行 IO 操作。

而創(chuàng)建HttpCodec對象的過程涉及到StreamAllocation风罩、RealConnection糠排,代碼較長,這里就不展開超升,這個過程概括來說入宦,就是找到一個可用的RealConnection,再利用RealConnection的輸入輸出(BufferedSourceBufferedSink)創(chuàng)建HttpCodec對象室琢,供后續(xù)步驟使用乾闰。

2.2.1.2.發(fā)送和接收數(shù)據(jù):CallServerInterceptor
@Override public Response intercept(Chain chain) throws IOException {
  HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
  StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
  Request request = chain.request();

  long sentRequestMillis = System.currentTimeMillis();
  httpCodec.writeRequestHeaders(request);

  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody);
    bufferedRequestBody.close();
  }

  httpCodec.finishRequest();

  Response response = httpCodec.readResponseHeaders()
      .request(request)
      .handshake(streamAllocation.connection().handshake())
      .sentRequestAtMillis(sentRequestMillis)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();

  if (!forWebSocket || response.code() != 101) {
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
  }

  if ("close".equalsIgnoreCase(response.request().header("Connection"))
      || "close".equalsIgnoreCase(response.header("Connection"))) {
    streamAllocation.noNewStreams();
  }

  // 省略部分檢查代碼

  return response;
}

我們抓住主干部分:

  1. 向服務(wù)器發(fā)送 request header;
  2. 如果有 request body盈滴,就向服務(wù)器發(fā)送涯肩;
  3. 讀取 response header,先構(gòu)造一個Response對象巢钓;
  4. 如果有 response body病苗,就在 3 的基礎(chǔ)上加上 body 構(gòu)造一個新的Response對象;

這里我們可以看到症汹,核心工作都由HttpCodec對象完成硫朦,而HttpCodec實際上利用的是 Okio,而 Okio 實際上還是用的Socket背镇,所以沒什么神秘的咬展,只不過一層套一層,層數(shù)有點多瞒斩。

其實Interceptor的設(shè)計也是一種分層的思想破婆,每個Interceptor就是一層。為什么要套這么多層呢胸囱?分層的思想在 TCP/IP 協(xié)議中就體現(xiàn)得淋漓盡致祷舀,分層簡化了每一層的邏輯,每層只需要關(guān)注自己的責(zé)任(單一原則思想也在此體現(xiàn)),而各層之間通過約定的接口/協(xié)議進行合作(面向接口編程思想)蔑鹦,共同完成復(fù)雜的任務(wù)夺克。

簡單應(yīng)該是我們的終極追求之一,盡管有時為了達成目標(biāo)不得不復(fù)雜嚎朽,但如果有另一種更簡單的方式铺纽,我想應(yīng)該沒有人不愿意替換。

2.2.2.發(fā)起異步網(wǎng)絡(luò)請求

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        System.out.println(response.body().string());
    }
});

// RealCall#enqueue
@Override public void enqueue(Callback responseCallback) {
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

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

這里我們就能看到 dispatcher 在異步執(zhí)行時發(fā)揮的作用了哟忍,如果當(dāng)前還能執(zhí)行一個并發(fā)請求狡门,那就立即執(zhí)行,否則加入readyAsyncCalls隊列锅很,而正在執(zhí)行的請求執(zhí)行完畢之后其馏,會調(diào)用promoteCalls()函數(shù),來把readyAsyncCalls隊列中的AsyncCall“提升”為runningAsyncCalls爆安,并開始執(zhí)行叛复。

這里的AsyncCallRealCall的一個內(nèi)部類,它實現(xiàn)了Runnable扔仓,所以可以被提交到ExecutorService上執(zhí)行褐奥,而它在執(zhí)行時會調(diào)用getResponseWithInterceptorChain()函數(shù),并把結(jié)果通過responseCallback傳遞給上層使用者翘簇。

這樣看來撬码,同步請求和異步請求的原理是一樣的,都是在getResponseWithInterceptorChain()函數(shù)中通過Interceptor鏈條來實現(xiàn)的網(wǎng)絡(luò)請求邏輯版保,而異步則是通過ExecutorService實現(xiàn)呜笑。

2.3返回數(shù)據(jù)的獲取

在上述同步(Call#execute()執(zhí)行之后)或者異步(Callback#onResponse()回調(diào)中)請求完成之后,我們就可以從Response對象中獲取到響應(yīng)數(shù)據(jù)了彻犁,包括 HTTP status code叫胁,status message,response header汞幢,response body 等曹抬。這里 body 部分最為特殊,因為服務(wù)器返回的數(shù)據(jù)可能非常大急鳄,所以必須通過數(shù)據(jù)流的方式來進行訪問(當(dāng)然也提供了諸如string()bytes()這樣的方法將流內(nèi)的數(shù)據(jù)一次性讀取完畢),而響應(yīng)中其他部分則可以隨意獲取堰酿。

響應(yīng) body 被封裝到ResponseBody類中疾宏,該類主要有兩點需要注意:

  1. 每個 body 只能被消費一次,多次消費會拋出異常触创;
  2. body 必須被關(guān)閉坎藐,否則會發(fā)生資源泄漏;

在2.2.1.2.發(fā)送和接收數(shù)據(jù):CallServerInterceptor小節(jié)中,我們就看過了 body 相關(guān)的代碼:

if (!forWebSocket || response.code() != 101) {
  response = response.newBuilder()
      .body(httpCodec.openResponseBody(response))
      .build();
}

HttpCodec#openResponseBody提供具體 HTTP 協(xié)議版本的響應(yīng) body岩馍,而HttpCodec則是利用 Okio 實現(xiàn)具體的數(shù)據(jù) IO 操作碉咆。

這里有一點值得一提,OkHttp 對響應(yīng)的校驗非常嚴(yán)格蛀恩,HTTP status line 不能有任何雜亂的數(shù)據(jù)疫铜,否則就會拋出異常,在我們公司項目的實踐中双谆,由于服務(wù)器的問題壳咕,偶爾 status line 會有額外數(shù)據(jù),而服務(wù)端的問題也毫無頭緒顽馋,導(dǎo)致我們不得不忍痛繼續(xù)使用 HttpUrlConnection谓厘,而后者在一些系統(tǒng)上又存在各種其他的問題,例如魅族系統(tǒng)發(fā)送 multi-part form 的時候就會出現(xiàn)沒有響應(yīng)的問題寸谜。

2.4.HTTP 緩存

在2.2.1.同步網(wǎng)絡(luò)請求小節(jié)中竟稳,我們已經(jīng)看到了Interceptor的布局,在建立連接熊痴、和服務(wù)器通訊之前他爸,就是CacheInterceptor,在建立連接之前愁拭,我們檢查響應(yīng)是否已經(jīng)被緩存讲逛、緩存是否可用,如果是則直接返回緩存的數(shù)據(jù)岭埠,否則就進行后面的流程盏混,并在返回之前,把網(wǎng)絡(luò)的數(shù)據(jù)寫入緩存惜论。

這塊代碼比較多许赃,但也很直觀,主要涉及 HTTP 協(xié)議緩存細節(jié)的實現(xiàn)馆类,而具體的緩存邏輯 OkHttp 內(nèi)置封裝了一個Cache類混聊,它利用DiskLruCache,用磁盤上的有限大小空間進行緩存乾巧,按照 LRU 算法進行緩存淘汰句喜,這里也不再展開。

我們可以在構(gòu)造OkHttpClient時設(shè)置Cache對象沟于,在其構(gòu)造函數(shù)中我們可以指定目錄和緩存大锌任浮:

public Cache(File directory, long maxSize);

而如果我們對 OkHttp 內(nèi)置的Cache類不滿意,我們可以自行實現(xiàn)InternalCache接口旷太,在構(gòu)造OkHttpClient時進行設(shè)置展懈,這樣就可以使用我們自定義的緩存策略了销睁。

三、總結(jié)

OkHttp 還有很多細節(jié)部分沒有在本文展開存崖,例如 HTTP2/HTTPS 的支持等冻记,但建立一個清晰的概覽非常重要。對整體有了清晰認識之后来惧,細節(jié)部分如有需要冗栗,再單獨深入將更加容易。

在文章最后我們再來回顧一下完整的流程圖:

image
  • OkHttpClient實現(xiàn)Call.Factory违寞,負責(zé)為Request創(chuàng)建Call贞瞒;
  • RealCall為具體的Call實現(xiàn),其enqueue()異步接口通過Dispatcher利用ExecutorService實現(xiàn)趁曼,而最終進行網(wǎng)絡(luò)請求時和同步execute()接口一致军浆,都是通過getResponseWithInterceptorChain()函數(shù)實現(xiàn);
  • getResponseWithInterceptorChain()中利用Interceptor鏈條挡闰,分層實現(xiàn)緩存乒融、透明壓縮、網(wǎng)絡(luò) IO 等功能摄悯;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赞季,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子奢驯,更是在濱河造成了極大的恐慌申钩,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘪阁,死亡現(xiàn)場離奇詭異撒遣,居然都是意外死亡,警方通過查閱死者的電腦和手機管跺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門义黎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人豁跑,你說我怎么就攤上這事廉涕。” “怎么了艇拍?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵狐蜕,是天一觀的道長。 經(jīng)常有香客問我卸夕,道長层释,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任娇哆,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碍讨。我一直安慰自己治力,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布勃黍。 她就那樣靜靜地躺著宵统,像睡著了一般。 火紅的嫁衣襯著肌膚如雪覆获。 梳的紋絲不亂的頭發(fā)上马澈,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音弄息,去河邊找鬼痊班。 笑死,一個胖子當(dāng)著我的面吹牛摹量,可吹牛的內(nèi)容都是我干的涤伐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼缨称,長吁一口氣:“原來是場噩夢啊……” “哼凝果!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起睦尽,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤器净,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后当凡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體山害,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年宁玫,在試婚紗的時候發(fā)現(xiàn)自己被綠了粗恢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡欧瘪,死狀恐怖眷射,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佛掖,我是刑警寧澤妖碉,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站芥被,受9級特大地震影響欧宜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拴魄,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一冗茸、第九天 我趴在偏房一處隱蔽的房頂上張望席镀。 院中可真熱鬧,春花似錦夏漱、人聲如沸豪诲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屎篱。三九已至,卻和暖如春葵蒂,著一層夾襖步出監(jiān)牢的瞬間交播,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工践付, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留秦士,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓荔仁,卻偏偏與公主長得像伍宦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乏梁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

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