深入理解OkHttp源碼及設計思想

用OkHttp很久了悉盆,也看了很多人寫的源碼分析心赶,在這里結(jié)合自己的感悟珊泳,記錄一下對OkHttp源碼理解的幾點心得严沥。

整體結(jié)構(gòu)

網(wǎng)絡請求框架雖然都要做請求任務的封裝和管理猜极,但是最大的難點在于網(wǎng)絡請求任務的多樣性,因為網(wǎng)絡層情況復雜消玄,不僅要考慮功能性的建立Socket連接跟伏、文件流傳輸、TLS安全翩瓜、多平臺等受扳,還要考慮性能上的Cache復用、Cache過期奥溺、連接池復用等辞色,這些功能如果交錯在一起,實現(xiàn)和維護都會有很大的問題浮定。

為了解決這個問題相满,OkHttp采用了分層設計的思想,使用多層攔截器桦卒,每個攔截器解決一個問題立美,多層攔截器套在一起,就像設計模式中的裝飾者模式一樣方灾,可以在保證每層功能高內(nèi)聚的情況下建蹄,解決多樣性的問題。

OkHttp使用了外觀模式裕偿,開發(fā)者直接操作的主要就是OkHttpClient洞慎,其實如果粗略劃分的話,整個OkHttp框架從功能上可以分為三部分:
1.請求和回調(diào):具體的類就是Call嘿棘、RealCall(及其內(nèi)部類AsyncCall)劲腿、Callback等。
2.分發(fā)器及線程池:具體的類就是Dispatcher鸟妙、ThreadPoolExecutor等焦人。
3.攔截器:實現(xiàn)了分層設計+鏈式調(diào)用,具體的類就是Interceptor+RealInterceptorChain重父。

至于更具體的操作花椭,均由攔截器實現(xiàn),包括應用層攔截器房午、網(wǎng)絡層攔截器等矿辽,開發(fā)者也可以自己擴展新的攔截器。

請求

網(wǎng)絡請求其實可以分為數(shù)據(jù)和行為兩部分郭厌,數(shù)據(jù)即我們的請求數(shù)據(jù)和返回數(shù)據(jù)嗦锐,行為則是發(fā)起網(wǎng)絡請求,以及得到處理結(jié)果沪曙。
數(shù)據(jù)(Request和Response)
在OkHttp中奕污,用Request定義請求數(shù)據(jù),用Response定義返回數(shù)據(jù)液走,這兩個類都使用了建造者模式碳默,把對象的創(chuàng)建和使用分離開,但這兩個類更接近于數(shù)據(jù)模型缘眶,主要用來讀寫數(shù)據(jù)嘱根,不做請求動作。
行為(Call/RealCall/AsyncCall和Callback)
在OkHttp中巷懈,用Call和Callback定義網(wǎng)絡請求该抒,用Call去發(fā)起網(wǎng)絡請求,用Callback去接收異步返回顶燕,(如果是同步請求凑保,就直接返回Response數(shù)據(jù))冈爹。
其中,Call是個接口欧引,真正的實現(xiàn)類是RealCall频伤,RealCall如果需要異步處理,還會先包裝為RealCall的內(nèi)部類AsyncCall芝此,然后再把AsyncCall交給線程池憋肖。

在具體執(zhí)行過程中,把數(shù)據(jù)對象交給行為對象去操作:
在RealCall行為中調(diào)用enqueue去發(fā)起異步網(wǎng)絡請求婚苹,此時需要傳參Request數(shù)據(jù)對象岸更;返回的Callback會傳遞Response數(shù)據(jù)對象。
如果RealCall行為中調(diào)用的是execute同步網(wǎng)絡請求膊升,就直接返回Response數(shù)據(jù)對象怎炊。

RealCall只是對請求做了封裝,真正處理請求的是分發(fā)器Dispatcher用僧。

分發(fā)器及線程池

對于網(wǎng)絡請求RealCall來說结胀,需要可并行、可回調(diào)责循、可取消糟港,因為OkHttp統(tǒng)一使用Dispatcher分發(fā)器來分發(fā)所有的Call請求,分發(fā)給多個線程進行執(zhí)行(所以Dispatcher也叫反向代理)院仿,所以秸抚,這幾個問題就需要交給Dispatcher來處理,對于Dispatcher來說歹垫,可并行剥汤、可回調(diào)、可取消的問題可以進一步被分解為以下幾個問題排惨,并分別處理:

1.有沒有必要管理所有的請求

不論是同步請求還是異步請求吭敢,都是耗時操作,所以是個需要觀測的行為暮芭,比如請求結(jié)束需要處理鹿驼,請求本身可能取消等,都需要管理起來辕宏。
而且畜晰,不論是正在運行的,還是等待運行的瑞筐,都需要管理凄鼻。

2.如何管理所有的請求

為了管理所有的請求,Dispatcher采用了隊列+生產(chǎn)+消費的模式。
為同步執(zhí)行提供了runningSyncCalls來管理所有的同步請求块蚌;
為異步執(zhí)行提供了runningAsyncCalls和readyAsyncCalls來管理所有的異步請求闰非。
其中readyAsyncCalls是在當前可用資源不足時,用于緩存請求的匈子。

由于這三個隊列的使用場景類似于棧河胎,偶爾需要刪除功能闯袒,所以OkHttp使用了ArrayDeque雙端隊列來管理虎敦,ArrayDeque的設計和實現(xiàn)非常精妙,感興趣的可以深入了解一下政敢。

3.如何確保多個隊列之間能順暢地調(diào)度

對于多線程情況下的隊列調(diào)度其徙,其實就是數(shù)據(jù)移動和失敗阻塞的這兩個問題。
對于數(shù)據(jù)移動來說喷户,就是要考慮多線程下隊列數(shù)據(jù)移動的問題唾那。
對于同步請求來說觉壶,只有1個隊列粘优,不存在數(shù)據(jù)移動,數(shù)據(jù)移動的場景在兩個異步隊列颜懊,每當有一個異步請求finish了河哑,就需要從待處理readyAsyncCalls隊列移動到runningAsyncCalls隊列避诽,這在多線程場景下并不安全,需要加鎖:

    synchronized (this) {//加鎖操作
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

在promoteCalls時璃谨,會把call從ready隊列轉(zhuǎn)移到running隊列:

 private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    ...
    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.
    }
  }

另外這個移動的操作放在finish函數(shù)里沙庐,會存在另一個問題,就是如何確保會執(zhí)行這個finish函數(shù)佳吞,避免造成失敗阻塞拱雏。

對于失敗阻塞來說,因為網(wǎng)絡請求失敗是很常見的場景底扳,必須能在失敗時避免阻塞隊列铸抑。
OkHttp的處理是為Call對象的execute函數(shù)寫try finally,在RealCall的execute函數(shù)里衷模,在finally中調(diào)用client.dispatcher.finish(call)鹊汛,確保隊列不阻塞。
這其實類似AsyncTask的處理方式算芯,AsyncTask也是使用了try finally柒昏,在finally中scheduleNext,確保隊列不阻塞熙揍。

4.如何實現(xiàn)多線程

io是個耗時但是不耗CPU的操作职祷,是典型的需要并行處理的場景。
OkHttp不出意外地采用了線程池實現(xiàn)并行,這一點類似于AsyncTask有梆,但不像AsyncTask使用了全局唯一的線程池是尖,每個OkHttpClient都有自己的線程池。
不過泥耀,與AsyncTask不同的是饺汹,OkHttp的同步執(zhí)行不進線程池,在RealCall執(zhí)行同步execute任務時痰催,只是在Dispatcher的runningSyncCalls中記錄這個call兜辞,然后直接在當前線程執(zhí)行了攔截器的操作。
至于異步執(zhí)行夸溶,就是在RealCall中enqueue時調(diào)用Dispatcher的enqueue逸吵,然后調(diào)用線程池executeService().execute(call),這里面的call是RealCall的內(nèi)部類AsyncCall缝裁,實現(xiàn)異步調(diào)用扫皱。

5.在這個過程中,用哪些方式提升效率

OkHttp主要針對隊列和線程池做了優(yōu)化:
循環(huán)數(shù)組
因為Dispatcher中的三個隊列需要頻繁出棧和入棧捷绑,所以采用了性能良好的循環(huán)數(shù)組ArrayDeque管理隊列韩脑。

阻塞隊列
因為Dispatcher自己用隊列管理了排隊的請求,所以Dispatcher中的線程池其實不需要緩存隊列粹污,那么這個線程池的任務其實是盡快地把元素轉(zhuǎn)交給線程池中的io線程段多,所以采用了容量為0的阻塞隊列SynchronousQueue,SynchronousQueue與普通隊列不同厕怜,不是數(shù)據(jù)等線程衩匣,而是線程等數(shù)據(jù),這樣每次向SynchronousQueue里傳入數(shù)據(jù)時粥航,都會立即交給一個線程執(zhí)行琅捏,這樣可以提高數(shù)據(jù)得到處理的速度。

控制線程數(shù)量
因為線程本身也會消耗資源递雀,所以每個線程池都需要控制線程數(shù)量柄延,OkHttp的線程池更進一步,會針對每個Host主機的請求(避免全都卡死在某個Host上)缀程,分別控制線程數(shù)上限(5個)搜吧,具體方法就是遍歷所有runningAsyncCall隊列中的每個Call,查詢每個Call的Host杨凑,并做計數(shù)滤奈。

攔截器原理

在前面的步驟中,不管是同步請求還是異步請求撩满,最終都會調(diào)用攔截器來處理網(wǎng)絡請求蜒程。

//RealCall源碼
Response result = getResponseWithInterceptorChain();

這就是OkHttp的核心绅你,Interceptor攔截器。
在OkHttp中昭躺,Call忌锯、Callback和Dispatcher雖然很有用,但對于解決復雜的網(wǎng)絡請求沒有太多作用领炫,使用了分層設計的攔截器Interceptor才是解決復雜網(wǎng)絡請求的核心偶垮,這也是OkHttp的核心設計。

分層設計

我們都知道帝洪,真實情況中的網(wǎng)絡行為其實非常復雜似舵,縱跨軟件、協(xié)議碟狞、數(shù)據(jù)包啄枕、電信號婚陪、硬件等族沃,所以網(wǎng)絡層的第一個基礎知識就是IOS七層模型,明確了各層的功能范圍泌参,每一層各司其職脆淹,層與層依次依賴,實際上降低了開發(fā)和維護的難度與成本沽一。

OkHttp也采用了分層設計思想盖溺,每層Interceptor的輸入都是Request,輸出都是Response铣缠,所以可以一層層地加工Request烘嘱,再一層層地加工Response。

由于各個Interceptor之間不是組合關系蝗蛙,不能像ViewTree那樣遞歸調(diào)用蝇庭,所以需要一個鏈把這些攔截器全部串起來,為此捡硅,入口RealCall會執(zhí)行網(wǎng)絡請求的getResponseWithInterceptorChain函數(shù)哮内,主要就是一層層地組織Interceptor,組成一個鏈壮韭,然后用chain.proceed去調(diào)用它北发。

  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()));//應用請求轉(zhuǎn)網(wǎng)絡請求
    interceptors.add(new CacheInterceptor(client.internalCache()));//緩存
    interceptors.add(new ConnectInterceptor(client));//連接
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//自定義網(wǎng)絡攔截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//服務端連接

    Interceptor.Chain chain = new RealInterceptorChain(//組成鏈
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);//從RealCall的Request開始鏈式處理
  }

如何實現(xiàn)鏈式處理

我們看到,鏈式處理的入口是RealInterceptorChain的proceed函數(shù):

  public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    ...
    RealInterceptorChain next = new RealInterceptorChain(//在chain中前進一步
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);//調(diào)用攔截器
    ...
    return response;
  }

而攔截器在執(zhí)行過程中喷屋,會再調(diào)用chain

  @Override 
  public Response intercept(Chain chain) throws IOException {
  ...
  Response networkResponse = chain.proceed(requestBuilder.build());
  ...

這樣琳拨,就形成一個chain.process(intreceptor)-->interceptor.intercept(chain)-->chainprocess(intreceptor)-->interceptor.intercept(chain)的循環(huán),這個過程中屯曹,chain不斷消費狱庇,直至最后一個攔截器寄疏,最后這個攔截器一定是CallServerInterceptor,CallServerInterceptor不再調(diào)用chain.process僵井,鏈式調(diào)用結(jié)束陕截。

攔截器的層次設計

了解過攔截器和鏈式反應的基本原理,我們再來看看各攔截器的層次設計和具體實現(xiàn)批什,有很多可以借鑒的地方农曲。
我們先回到RealCall中,看看攔截器的層次和分類:

  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()));//應用請求轉(zhuǎn)網(wǎng)絡請求
    interceptors.add(new CacheInterceptor(client.internalCache()));//緩存
    interceptors.add(new ConnectInterceptor(client));//連接
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//自定義網(wǎng)絡攔截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//實現(xiàn)在線網(wǎng)絡連接

    Interceptor.Chain chain = new RealInterceptorChain(//組成鏈
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);//從RealCall的Request開始鏈式處理
  }

我們可以看到驻债,OkHttp中攔截器的層次是這樣的:
1.自定義應用攔截器
2.重試乳规、重定向攔截器
3.應用/網(wǎng)絡橋接攔截器
4.緩存攔截器
5.連接攔截器
6.自定義網(wǎng)絡攔截器
7.在線網(wǎng)絡請求攔截器

我們看到,我們開發(fā)者可以添加兩種自定義Interceptor合呐,一種是client.interceptors()應用層攔截器暮的,一種是client.networkInterceptors()網(wǎng)絡層攔截器
但其實這兩種都是Interceptor淌实,為什么可以分成是應用層和網(wǎng)絡層呢冻辩?
因為在網(wǎng)絡層攔截器上方,是ConnectionInterceptor連接攔截器拆祈,這個攔截器里會提供Address恨闪、ConnectionPool等資源,可以用于處理網(wǎng)絡連接放坏,networkInterceptors是添加在這之后的咙咽,可以參與真正的網(wǎng)絡層數(shù)據(jù)的處理。
接下來淤年,我們自頂向下钧敞,依次看看每層攔截器的實現(xiàn)

攔截器——自定義應用攔截器

OkHttp在最外圍允許添加自定義的應用攔截器,我們可以攔截Request和Response麸粮,分別進行加工溉苛,例如在Request時統(tǒng)一添加Header和Url參數(shù):

Request.Builder builder = chain.request().newBuilder();
builder.addHeader("Accept-Charset", "UTF-8");
builder.addHeader("Accept", " application/json");
builder.addHeader("Content-type", "application/json");

HttpUrl url=builder.build().url().newBuilder()
                  .addQueryParameter("mac", EquipmentUtils.getMac())
                  .build();
Request request = builder.url(url).build();

還可以攔截Response內(nèi)容,打印返回數(shù)據(jù)的日志:

long t1 = System.nanoTime();
Request request = chain.request();
Response response = chain.proceed(request);
long t2 = System.nanoTime();

//直接復制字節(jié)流豹休,獲取response的數(shù)據(jù)內(nèi)容
BufferedSource sr = response.body().source();
sr.request(Long.MAX_VALUE);
Buffer buf = sr.buffer().clone();//copy副本讀取炊昆,不能讀取原文
String content = buf.readString(Charset.forName("UTF-8"));
buf.clear();

Log.i(TAG, "net layer received response of url: " + request.url().url().toString()
          + "\nresponse: " + content
          + "\nspent time: " + (t2 - t1) / 1e6d);

開發(fā)者可以擴展針對請求數(shù)據(jù)和返回數(shù)據(jù),自由開發(fā)功能威根。

攔截器——重試/重定向

雖然前面有開發(fā)者自定義的應用攔截器凤巨,但是真正準備處理網(wǎng)絡連接,是從OkHttp自己定義的RetryAndFollowUpInterceptor開始的洛搀,因為OkHttp正是把這個攔截器作為真正的入口敢茁,創(chuàng)建StreamAllocation對象,在StreamAllocation對象中準備了網(wǎng)絡連接的Address留美、連接池等資源彰檬,后續(xù)的攔截器伸刃,使用的都是這個StreamAllocation對象。

StreanAllocation

StreamAllocation是OkHttp中用來定義和傳遞網(wǎng)絡資源逢倍,并建立網(wǎng)絡連接的對象捧颅,內(nèi)部包含:
Address:規(guī)定如何連接服務器,包括DNS较雕、協(xié)議碉哑、URL等。
Route:存儲建立連接的目標IP和端口InetSocketAddress亮蒋,以及代理服務器扣典。
ConnectionPool:存儲和復用已存在的連接,復用時根據(jù)Address查找對應的連接慎玖。
StreamAllocation會通過findConnection創(chuàng)建連接贮尖,或復用已存在的連接,期間會調(diào)用RealConnection趁怔,根據(jù)設置建立TLS連接湿硝、處理握手協(xié)議等,最底層是根據(jù)當前運行的平臺痕钢,直接操作Socket图柏。
每個Host不超過5個連接,每個連接不超過5分鐘任连。

重試/重定向

網(wǎng)絡環(huán)境本質(zhì)上是不穩(wěn)定的,已建立的連接可能突然不可用例诀,或者連接可用但是服務器報錯随抠,這就需要重試/重定向功能,這也是RetryAndFollowUpInterceptor攔截器的分層功能繁涂。
重試
如果整個鏈式調(diào)用出現(xiàn)了RouteException或IOException拱她,就會調(diào)用recover函數(shù)重新建立連接;
重定向
如果服務器返回錯誤碼如301扔罪,要求重定向秉沼,就會調(diào)用followUpRequest函數(shù),新建一個Request矿酵,然后重定向唬复,再走一遍整個調(diào)用鏈。
while
intercept函數(shù)中的這些主要邏輯都在while(true)循環(huán)中全肮,最大循環(huán)上限是20敞咧。

攔截器——應用轉(zhuǎn)網(wǎng)絡的橋接功能

BridgeInterceptor是個橋梁,這主要是指他會自動處理一些網(wǎng)絡層特有的Header信息辜腺,例如Host屬性休建,是HTTP1.1必須的乍恐,但應用層并不關心這個屬性,這就是由BridgeInterceptor自動處理的测砂。
BridgeInterceptor中處理的Header屬性包括Host茵烈、Connection的Keep-Alive、gzip透明壓縮砌些、User-Agent描述瞧毙、Cookie策略等。
當然寄症,因為OkHttp采用了外觀模式宙彪,所以很多屬性需要通過client設置和獲取。

攔截器——緩存功能

在網(wǎng)絡請求中使用緩存是非常必要提速手段有巧,OkHttp專門用了CacheInterceptor攔截器來處理這個功能释漆。
緩存的使用注意包括存儲、查詢和有效性檢查篮迎,在OkHttp中:
存儲男图,使用client外觀模式來設置存儲Cache數(shù)據(jù)的InternalCache實現(xiàn)類,在走請求鏈獲取Response時記錄cache甜橱。
查詢逊笆,在存儲Cache數(shù)據(jù)的InternalCache實現(xiàn)類中,根據(jù)Request過濾岂傲,來查找Cache难裆。
有效性檢查,利用工具類CacheStrategy的getCandidate函數(shù)镊掖,來判斷Cache數(shù)據(jù)的各項指標是否達到條件乃戈。

攔截器——連接功能

在RetryAndFollowUpInterceptor入口處,我們已經(jīng)分析過亩进,在OkHttp中症虑,連接功能由StreamAlloc實現(xiàn),提供Address地址归薛、Route路由谍憔、RealConnection連接、ConnectionPool線程池復用主籍、身份驗證习贫、協(xié)議、握手崇猫、平臺沈条、安全等功能。

在ConnectionInterceptor這一層诅炉,其實還沒有真正連接網(wǎng)絡蜡歹,它的具體功能很簡單屋厘,就是準備好request請求、streamAllocation連接資源月而、httpCodec傳輸工具汗洒、connection連接,為最底層的網(wǎng)絡連接服務父款。

其中溢谤,httpCodec通過sink提供了OKio封裝過的基于socket的OutputStream,通過source提供了OKio封裝的基于socket的InputStream憨攒,最終就是通過這個sink提交Request世杀,用這個source獲取Response。

攔截器——自定義網(wǎng)絡攔截器

主要區(qū)別

自定義的網(wǎng)絡層攔截器相比應用層攔截器肝集,能直接監(jiān)測到在線網(wǎng)絡請求的數(shù)據(jù)交換過程瞻坝。
例如,Http有url重定向機制杏瞻,如果Http返回碼為301所刀,就需要根據(jù)Header中Location字段的新url,重新發(fā)起一次請求捞挥,這樣的話浮创,總共會有兩次請求。

在應用層的攔截器看來砌函,第一次請求并沒有返回有效數(shù)據(jù)斩披,它只會抓到一次請求,也就是第二次的請求胸嘴。
但是在網(wǎng)絡層的攔截器看來雏掠,兩次都是網(wǎng)絡請求,所以它會抓到兩次請求劣像。

用途擴展

根據(jù)網(wǎng)絡層攔截器的特點,我們可以擴展如下功能:
1.模擬各種網(wǎng)絡情況
網(wǎng)絡接口不只是可用不可用的問題摧玫,還存在速度波動的問題耳奕,一個穩(wěn)健的App應該能hold住波動的甚至是斷斷續(xù)續(xù)的網(wǎng)絡,但是這樣的網(wǎng)絡非常不好模擬诬像,我們可以在網(wǎng)絡攔截器層自由設定網(wǎng)絡返回值和返回時間屋群,輔助我們檢查App在處理網(wǎng)絡數(shù)據(jù)時的健壯性。
2.模擬多個備用地址切換
無論是為了災備坏挠,還是為了節(jié)省DNS解析時間芍躏,App都會有多個備用地址,有些就是ip地址降狠,當網(wǎng)絡出現(xiàn)問題時对竣,要自動切換到備用地址庇楞,就可以在網(wǎng)絡層模擬出301返回,直接重定向到備用地址否纬。
3.模擬數(shù)據(jù)輔助開發(fā)/測試
在開發(fā)過程中吕晌,我們可以用gradle多環(huán)境的方法,增加一個mock的productFlavor临燃,在這個環(huán)境下添加一個mockInterceptor睛驳,把指向官網(wǎng)的地址重定向為指向開發(fā)測試網(wǎng)址,甚至直接mock返回數(shù)據(jù)膜廊,換掉在線數(shù)據(jù)乏沸,這樣可以檢測整個網(wǎng)絡層的全部功能(編碼、緩存爪瓜、切換蹬跃、報錯等),把mock數(shù)據(jù)的內(nèi)容和App的反饋結(jié)合的話钥勋,還可以做到針對網(wǎng)絡數(shù)據(jù)的半自動/自動化的測試驗證炬转。

攔截器——在線網(wǎng)絡請求功能

前面所有的攔截器,都是在準備或處理網(wǎng)絡連接前后的數(shù)據(jù)算灸,只有CallServerInterceptor這個攔截器扼劈,是真正連接在線服務的。
它使用ConnectionInterceptor提供的HttpCodec傳輸工具來發(fā)出Request菲驴,獲取Response荐吵,然后用ResponseBuilder生成最終的Response,再層層傳遞給外層的攔截器赊瞬。
HttpCodec本身是一個接口先煎,實例是StreamAllocation利用RealConnection生產(chǎn)的,RealConnection根據(jù)連接池中的可用連接巧涧,利用Okio生產(chǎn)source和sink:

  private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    Proxy proxy = route.proxy();
    Address address = route.address();

    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);

    rawSocket.setSoTimeout(readTimeout);
    ...
      //用Okio生產(chǎn)
      source = Okio.buffer(Okio.source(rawSocket));
      sink = Okio.buffer(Okio.sink(rawSocket));
    ...
  }

Okio的source是socket.inputStream薯蝎,sink是socket.outputStream。
所以谤绳,真正在傳輸數(shù)據(jù)時占锯,就是用Okio的sink去傳socket,用source去取socket缩筛,底層其實也是socket操作消略。

其他特性

以上是OkHttp的主要內(nèi)容,此外瞎抛,OkHttp還有一些很有意思的特性艺演。

1.返回數(shù)據(jù)閱后即焚

在OkHttp中,如果要攔截ResponseBody的數(shù)據(jù)內(nèi)容(比如寫日志),會發(fā)現(xiàn)該數(shù)據(jù)讀過一次就會被情況胎撤,相當于是“閱后即焚”:

  //ResponseBody源碼
  public final String string() throws IOException { //底層不能自己消化異常晓殊,應該向上層拋出異常
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    //不做catch,異常全部拋出給上層
    } finally { //確保原始字節(jié)數(shù)據(jù)得到處理
      Util.closeQuietly(source); //閱后即焚哩照,這樣可以迅速騰出內(nèi)存空間來
    }
  }

如果一定要攔截出數(shù)據(jù)內(nèi)容挺物,我們就不能直接讀ResponseBody中的source,需要copy一個副本才行:

BufferedSource sr = response.body().source();
sr.request(Long.MAX_VALUE);
Buffer buf = sr.buffer().clone();//copy副本讀取飘弧,不能讀取原文
String content = buf.readString(Charset.forName("UTF-8"));
buf.clear();

Response也提供了專門獲取ResponsBody數(shù)據(jù)的函數(shù)peekBody识藤,實現(xiàn)原理也是copy“:

  //Response源碼
  public ResponseBody peekBody(long byteCount) throws IOException {
    BufferedSource source = body.source();
    source.request(byteCount);
    Buffer copy = source.buffer().clone();
    ...
    return ResponseBody.create(body.contentType(), result.size(), result);
  }

參考

深入解析OkHttp3
OkHttp3源碼分析[綜述]
Okhttp-wiki 之 Interceptors 攔截器

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市次伶,隨后出現(xiàn)的幾起案子痴昧,更是在濱河造成了極大的恐慌,老刑警劉巖冠王,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赶撰,死亡現(xiàn)場離奇詭異,居然都是意外死亡柱彻,警方通過查閱死者的電腦和手機豪娜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哟楷,“玉大人瘤载,你說我怎么就攤上這事÷羯茫” “怎么了鸣奔?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惩阶。 經(jīng)常有香客問我挎狸,道長,這世上最難降的妖魔是什么断楷? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任锨匆,我火速辦了婚禮,結(jié)果婚禮上冬筒,老公的妹妹穿的比我還像新娘统刮。我一直安慰自己,他們只是感情好账千,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著暗膜,像睡著了一般匀奏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上学搜,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天娃善,我揣著相機與錄音论衍,去河邊找鬼。 笑死聚磺,一個胖子當著我的面吹牛坯台,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瘫寝,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蜒蕾,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了焕阿?” 一聲冷哼從身側(cè)響起咪啡,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎暮屡,沒想到半個月后撤摸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡褒纲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年准夷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片莺掠。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡衫嵌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出汁蝶,到底是詐尸還是另有隱情渐扮,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布掖棉,位于F島的核電站墓律,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏幔亥。R本人自食惡果不足惜耻讽,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帕棉。 院中可真熱鬧针肥,春花似錦、人聲如沸香伴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽即纲。三九已至具帮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜂厅。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工匪凡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掘猿。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓病游,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稠通。 傳聞我的和親對象是個殘疾皇子衬衬,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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