深入理解OkHttp原始碼及設計思想

Java小鋪發表於2018-08-20

前言

用OkHttp很久了,也看了很多人寫的原始碼分析,在這裡結合自己的感悟,記錄一下對OkHttp原始碼理解的幾點心得。

整體結構

網路請求框架雖然都要做請求任務的封裝和管理,但是最大的難點在於網路請求任務的多樣性,因為網路層情況複雜,不僅要考慮功能性的建立Socket連線、檔案流傳輸、TLS安全、多平臺等,還要考慮效能上的Cache複用、Cache過期、連線池複用等,這些功能如果交錯在一起,實現和維護都會有很大的問題。

為了解決這個問題,OkHttp採用了分層設計的思想,使用多層攔截器,每個攔截器解決一個問題,多層攔截器套在一起,就像設計模式中的裝飾者模式一樣,可以在保證每層功能高內聚的情況下,解決多樣性的問題。

OkHttp使用了外觀模式,開發者直接操作的主要就是OkHttpClient,其實如果粗略劃分的話,整個OkHttp框架從功能上可以分為三部分:

1.請求和回撥:具體的類就是Call、RealCall(及其內部類AsyncCall)、Callback等。

2.分發器及執行緒池:具體的類就是Dispatcher、ThreadPoolExecutor等。

3.攔截器:實現了分層設計+鏈式呼叫,具體的類就是Interceptor+RealInterceptorChain。

至於更具體的操作,均由攔截器實現,包括應用層攔截器、網路層攔截器等,開發者也可以自己擴充套件新的攔截器。

請求

網路請求其實可以分為資料和行為兩部分,資料即我們的請求資料和返回資料,行為則是發起網路請求,以及得到處理結果。

資料(Request和Response)

在OkHttp中,用Request定義請求資料,用Response定義返回資料,這兩個類都使用了建造者模式,把物件的建立和使用分離開,但這兩個類更接近於資料模型,主要用來讀寫資料,不做請求動作。

行為(Call/RealCall/AsyncCall和Callback)

在OkHttp中,用Call和Callback定義網路請求,用Call去發起網路請求,用Callback去接收非同步返回,(如果是同步請求,就直接返回Response資料)。

其中,Call是個介面,真正的實現類是RealCall,RealCall如果需要非同步處理,還會先包裝為RealCall的內部類AsyncCall,然後再把AsyncCall交給執行緒池。

在具體執行過程中,把資料物件交給行為物件去操作:

在RealCall行為中呼叫enqueue去發起非同步網路請求,此時需要傳參Request資料物件;返回的Callback會傳遞Response資料物件。

如果RealCall行為中呼叫的是execute同步網路請求,就直接返回Response資料物件。

RealCall只是對請求做了封裝,真正處理請求的是分發器Dispatcher。

分發器及執行緒池

對於網路請求RealCall來說,需要可並行、可回撥、可取消,因為OkHttp統一使用Dispatcher分發器來分發所有的Call請求,分發給多個執行緒進行執行(所以Dispatcher也叫反向代理),所以,這幾個問題就需要交給Dispatcher來處理,對於Dispatcher來說,可並行、可回撥、可取消的問題可以進一步被分解為以下幾個問題,並分別處理:

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

不論是同步請求還是非同步請求,都是耗時操作,所以是個需要觀測的行為,比如請求結束需要處理,請求本身可能取消等,都需要管理起來。

而且,不論是正在執行的,還是等待執行的,都需要管理。

2.如何管理所有的請求

為了管理所有的請求,Dispatcher採用了佇列+生產+消費的模式。

為同步執行提供了runningSyncCalls來管理所有的同步請求;

為非同步執行提供了runningAsyncCalls和readyAsyncCalls來管理所有的非同步請求。

其中readyAsyncCalls是在當前可用資源不足時,用於快取請求的。

由於這三個佇列的使用場景類似於棧,偶爾需要刪除功能,所以OkHttp使用了ArrayDeque雙端佇列來管理,ArrayDeque的設計和實現非常精妙,感興趣的可以深入瞭解一下。

www.jianshu.com/p/132733115…

3.如何確保多個佇列之間能順暢地排程

對於多執行緒情況下的佇列排程,其實就是資料移動和失敗阻塞的這兩個問題。

對於資料移動來說,就是要考慮多執行緒下佇列資料移動的問題。

對於同步請求來說,只有1個佇列,不存在資料移動,資料移動的場景在兩個非同步佇列,每當有一個非同步請求finish了,就需要從待處理readyAsyncCalls佇列移動到runningAsyncCalls佇列,這在多執行緒場景下並不安全,需要加鎖:

synchronized (this) {//加鎖操作

if(!calls.remove(call))thrownewAssertionError("Call wasn't in-flight!");

if(promoteCalls) promoteCalls();

runningCallsCount = runningCallsCount();

idleCallback =this.idleCallback;

}

在promoteCalls時,會把call從ready佇列轉移到running佇列:

privatevoidpromoteCalls(){

if(runningAsyncCalls.size() >= maxRequests)return;// Already running max capacity.

...

for(Iterator 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函式裡,會存在另一個問題,就是如何確保會執行這個finish函式,避免造成失敗阻塞。

對於失敗阻塞來說,因為網路請求失敗是很常見的場景,必須能在失敗時避免阻塞佇列。

OkHttp的處理是為Call物件的execute函式寫try finally,在RealCall的execute函式裡,在finally中呼叫client.dispatcher.finish(call),確保佇列不阻塞。

這其實類似AsyncTask的處理方式,AsyncTask也是使用了try finally,在finally中scheduleNext,確保佇列不阻塞。

4.如何實現多執行緒

io是個耗時但是不耗CPU的操作,是典型的需要並行處理的場景。

OkHttp不出意外地採用了執行緒池實現並行,這一點類似於AsyncTask,但不像AsyncTask使用了全域性唯一的執行緒池,每個OkHttpClient都有自己的執行緒池。

不過,與AsyncTask不同的是,OkHttp的同步執行不進執行緒池,在RealCall執行同步execute任務時,只是在Dispatcher的runningSyncCalls中記錄這個call,然後直接在當前執行緒執行了攔截器的操作。

至於非同步執行,就是在RealCall中enqueue時呼叫Dispatcher的enqueue,然後呼叫執行緒池executeService().execute(call),這裡面的call是RealCall的內部類AsyncCall,實現非同步呼叫。

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

OkHttp主要針對佇列和執行緒池做了優化:

迴圈陣列

因為Dispatcher中的三個佇列需要頻繁出棧和入棧,所以採用了效能良好的迴圈陣列ArrayDeque管理佇列。

阻塞佇列

因為Dispatcher自己用佇列管理了排隊的請求,所以Dispatcher中的執行緒池其實不需要快取佇列,那麼這個執行緒池的任務其實是儘快地把元素轉交給執行緒池中的io執行緒,所以採用了容量為0的阻塞佇列SynchronousQueue,SynchronousQueue與普通佇列不同,不是資料等執行緒,而是執行緒等資料,這樣每次向SynchronousQueue裡傳入資料時,都會立即交給一個執行緒執行,這樣可以提高資料得到處理的速度。

控制執行緒數量

因為執行緒本身也會消耗資源,所以每個執行緒池都需要控制執行緒數量,OkHttp的執行緒池更進一步,會針對每個Host主機的請求(避免全都卡死在某個Host上),分別控制執行緒數上限(5個),具體方法就是遍歷所有runningAsyncCall佇列中的每個Call,查詢每個Call的Host,並做計數。

攔截器原理

在前面的步驟中,不管是同步請求還是非同步請求,最終都會呼叫攔截器來處理網路請求。

//RealCall原始碼

Response result = getResponseWithInterceptorChain();

這就是OkHttp的核心,Interceptor攔截器。

在OkHttp中,Call、Callback和Dispatcher雖然很有用,但對於解決複雜的網路請求沒有太多作用,使用了分層設計的攔截器Interceptor才是解決複雜網路請求的核心,這也是OkHttp的核心設計。

分層設計

我們都知道,真實情況中的網路行為其實非常複雜,縱跨軟體、協議、資料包、電訊號、硬體等,所以網路層的第一個基礎知識就是IOS七層模型,明確了各層的功能範圍,每一層各司其職,層與層依次依賴,實際上降低了開發和維護的難度與成本。

OkHttp也採用了分層設計思想,每層Interceptor的輸入都是Request,輸出都是Response,所以可以一層層地加工Request,再一層層地加工Response。

由於各個Interceptor之間不是組合關係,不能像ViewTree那樣遞迴呼叫,所以需要一個鏈把這些攔截器全部串起來,為此,入口RealCall會執行網路請求的getResponseWithInterceptorChain函式,主要就是一層層地組織Interceptor,組成一個鏈,然後用chain.proceed去呼叫它。

ResponsegetResponseWithInterceptorChain() throws IOException{

// Build a full stack of interceptors.

List interceptors =newArrayList<>();

interceptors.addAll(client.interceptors());//自定義應用攔截器

interceptors.add(retryAndFollowUpInterceptor);//重試/重定向

interceptors.add(newBridgeInterceptor(client.cookieJar()));//應用請求轉網路請求

interceptors.add(newCacheInterceptor(client.internalCache()));//快取

interceptors.add(newConnectInterceptor(client));//連線

if(!forWebSocket) {

interceptors.addAll(client.networkInterceptors());//自定義網路攔截器

}

interceptors.add(newCallServerInterceptor(forWebSocket));//服務端連線

Interceptor.Chain chain =newRealInterceptorChain(//組成鏈

interceptors,null,null,null,0, originalRequest);

returnchain.proceed(originalRequest);//從RealCall的Request開始鏈式處理

}

如何實現鏈式處理

我們看到,鏈式處理的入口是RealInterceptorChain的proceed函式:

publicResponseproceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,

RealConnection connection

) throws IOException{

...

RealInterceptorChain next =newRealInterceptorChain(//在chain中前進一步

interceptors, streamAllocation, httpCodec, connection, index +1, request);

Interceptor interceptor = interceptors.get(index);

Response response = interceptor.intercept(next);//呼叫攔截器

...

returnresponse;

}

而攔截器在執行過程中,會再呼叫chain

@Override

publicResponseintercept(Chain chain)throwsIOException{

...

Response networkResponse = chain.proceed(requestBuilder.build());

...

這樣,就形成一個chain.process(intreceptor)-->interceptor.intercept(chain)-->chainprocess(intreceptor)-->interceptor.intercept(chain)的迴圈,這個過程中,chain不斷消費,直至最後一個攔截器,最後這個攔截器一定是CallServerInterceptor,CallServerInterceptor不再呼叫chain.process,鏈式呼叫結束。

攔截器的層次設計

瞭解過攔截器和鏈式反應的基本原理,我們再來看看各攔截器的層次設計和具體實現,有很多可以借鑑的地方。

我們先回到RealCall中,看看攔截器的層次和分類:

ResponsegetResponseWithInterceptorChain() throws IOException{

// Build a full stack of interceptors.

List interceptors =newArrayList<>();

interceptors.addAll(client.interceptors());//自定義應用攔截器

interceptors.add(retryAndFollowUpInterceptor);//重試/重定向

interceptors.add(newBridgeInterceptor(client.cookieJar()));//應用請求轉網路請求

interceptors.add(newCacheInterceptor(client.internalCache()));//快取

interceptors.add(newConnectInterceptor(client));//連線

if(!forWebSocket) {

interceptors.addAll(client.networkInterceptors());//自定義網路攔截器

}

interceptors.add(newCallServerInterceptor(forWebSocket));//實現線上網路連線

Interceptor.Chain chain =newRealInterceptorChain(//組成鏈

interceptors,null,null,null,0, originalRequest);

returnchain.proceed(originalRequest);//從RealCall的Request開始鏈式處理

}

我們可以看到,OkHttp中攔截器的層次是這樣的:

1.自定義應用攔截器

2.重試、重定向攔截器

3.應用/網路橋接攔截器

4.快取攔截器

5.連線攔截器

6.自定義網路攔截器

7.線上網路請求攔截器

我們看到,我們開發者可以新增兩種自定義Interceptor,一種是client.interceptors()應用層攔截器,一種是client.networkInterceptors()網路層攔截器。

但其實這兩種都是Interceptor,為什麼可以分成是應用層和網路層呢?

因為在網路層攔截器上方,是ConnectionInterceptor連線攔截器,這個攔截器裡會提供Address、ConnectionPool等資源,可以用於處理網路連線,networkInterceptors是新增在這之後的,可以參與真正的網路層資料的處理。

接下來,我們自頂向下,依次看看每層攔截器的實現

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

OkHttp在最外圍允許新增自定義的應用攔截器,我們可以攔截Request和Response,分別進行加工,例如在Request時統一新增Header和Url引數:

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();

Requestrequest= builder.url(url).build();

還可以攔截Response內容,列印返回資料的日誌:

longt1 = System.nanoTime();

Request request = chain.request();

Response response = chain.proceed(request);

longt2 = System.nanoTime();

//直接複製位元組流,獲取response的資料內容

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);

開發者可以擴充套件針對請求資料和返回資料,自由開發功能。

攔截器——重試/重定向

雖然前面有開發者自定義的應用攔截器,但是真正準備處理網路連線,是從OkHttp自己定義的RetryAndFollowUpInterceptor開始的,因為OkHttp正是把這個攔截器作為真正的入口,建立StreamAllocation物件,在StreamAllocation物件中準備了網路連線的Address、連線池等資源,後續的攔截器,使用的都是這個StreamAllocation物件。

StreanAllocation

StreamAllocation是OkHttp中用來定義和傳遞網路資源,並建立網路連線的物件,內部包含:

Address:規定如何連線伺服器,包括DNS、協議、URL等。

Route:儲存建立連線的目標IP和埠InetSocketAddress,以及代理伺服器。

ConnectionPool:儲存和複用已存在的連線,複用時根據Address查詢對應的連線。

StreamAllocation會通過findConnection建立連線,或複用已存在的連線,期間會呼叫RealConnection,根據設定建立TLS連線、處理握手協議等,最底層是根據當前執行的平臺,直接操作Socket。

每個Host不超過5個連線,每個連線不超過5分鐘。

重試/重定向

網路環境本質上是不穩定的,已建立的連線可能突然不可用,或者連線可用但是伺服器報錯,這就需要重試/重定向功能,這也是RetryAndFollowUpInterceptor攔截器的分層功能。

重試

如果整個鏈式呼叫出現了RouteException或IOException,就會呼叫recover函式重新建立連線;

重定向

如果伺服器返回錯誤碼如301,要求重定向,就會呼叫followUpRequest函式,新建一個Request,然後重定向,再走一遍整個呼叫鏈。

while

intercept函式中的這些主要邏輯都在while(true)迴圈中,最大迴圈上限是20。

攔截器——應用轉網路的橋接功能

BridgeInterceptor是個橋樑,這主要是指他會自動處理一些網路層特有的Header資訊,例如Host屬性,是HTTP1.1必須的,但應用層並不關心這個屬性,這就是由BridgeInterceptor自動處理的。

BridgeInterceptor中處理的Header屬性包括Host、Connection的Keep-Alive、gzip透明壓縮、User-Agent描述、Cookie策略等。

當然,因為OkHttp採用了外觀模式,所以很多屬性需要通過client設定和獲取。

攔截器——快取功能

在網路請求中使用快取是非常必要提速手段,OkHttp專門用了CacheInterceptor攔截器來處理這個功能。

快取的使用注意包括儲存、查詢和有效性檢查,在OkHttp中:

儲存,使用client外觀模式來設定儲存Cache資料的InternalCache實現類,在走請求鏈獲取Response時記錄cache。

查詢,在儲存Cache資料的InternalCache實現類中,根據Request過濾,來查詢Cache。

有效性檢查,利用工具類CacheStrategy的getCandidate函式,來判斷Cache資料的各項指標是否達到條件。

攔截器——連線功能

在RetryAndFollowUpInterceptor入口處,我們已經分析過,在OkHttp中,連線功能由StreamAlloc實現,提供Address地址、Route路由、RealConnection連線、ConnectionPool執行緒池複用、身份驗證、協議、握手、平臺、安全等功能。

在ConnectionInterceptor這一層,其實還沒有真正連線網路,它的具體功能很簡單,就是準備好request請求、streamAllocation連線資源、httpCodec傳輸工具、connection連線,為最底層的網路連線服務。

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

攔截器——自定義網路攔截器

主要區別

自定義的網路層攔截器相比應用層攔截器,能直接監測到線上網路請求的資料交換過程。

例如,Http有url重定向機制,如果Http返回碼為301,就需要根據Header中Location欄位的新url,重新發起一次請求,這樣的話,總共會有兩次請求。

在應用層的攔截器看來,第一次請求並沒有返回有效資料,它只會抓到一次請求,也就是第二次的請求。

但是在網路層的攔截器看來,兩次都是網路請求,所以它會抓到兩次請求。

用途擴充套件

根據網路層攔截器的特點,我們可以擴充套件如下功能:

1.模擬各種網路情況

網路介面不只是可用不可用的問題,還存在速度波動的問題,一個穩健的App應該能hold住波動的甚至是斷斷續續的網路,但是這樣的網路非常不好模擬,我們可以在網路攔截器層自由設定網路返回值和返回時間,輔助我們檢查App在處理網路資料時的健壯性。

2.模擬多個備用地址切換

無論是為了災備,還是為了節省DNS解析時間,App都會有多個備用地址,有些就是ip地址,當網路出現問題時,要自動切換到備用地址,就可以在網路層模擬出301返回,直接重定向到備用地址。

3.模擬資料輔助開發/測試

在開發過程中,我們可以用gradle多環境的方法,增加一個mock的productFlavor,在這個環境下新增一個mockInterceptor,把指向官網的地址重定向為指向開發測試網址,甚至直接mock返回資料,換掉線上資料,這樣可以檢測整個網路層的全部功能(編碼、快取、切換、報錯等),把mock資料的內容和App的反饋結合的話,還可以做到針對網路資料的半自動/自動化的測試驗證。

攔截器——線上網路請求功能

前面所有的攔截器,都是在準備或處理網路連線前後的資料,只有CallServerInterceptor這個攔截器,是真正連線線上服務的。

它使用ConnectionInterceptor提供的HttpCodec傳輸工具來發出Request,獲取Response,然後用ResponseBuilder生成最終的Response,再層層傳遞給外層的攔截器。

HttpCodec本身是一個介面,例項是StreamAllocation利用RealConnection生產的,RealConnection根據連線池中的可用連線,利用Okio生產source和sink:

privatevoidconnectSocket(intconnectTimeout,intreadTimeout)throwsIOException{

Proxy proxy = route.proxy();

Address address = route.address();

rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP

? address.socketFactory().createSocket()

:newSocket(proxy);

rawSocket.setSoTimeout(readTimeout);

...

//用Okio生產

source = Okio.buffer(Okio.source(rawSocket));

sink = Okio.buffer(Okio.sink(rawSocket));

...

}

Okio的source是socket.inputStream,sink是socket.outputStream。

所以,真正在傳輸資料時,就是用Okio的sink去傳socket,用source去取socket,底層其實也是socket操作。

其他特性

以上是OkHttp的主要內容,此外,OkHttp還有一些很有意思的特性。

1.返回資料閱後即焚

在OkHttp中,如果要攔截ResponseBody的資料內容(比如寫日誌),會發現該資料讀過一次就會被情況,相當於是“閱後即焚:

//ResponseBody原始碼

publicfinalStringstring()throwsIOException{//底層不能自己消化異常,應該向上層丟擲異常

BufferedSource source = source();

try{

Charset charset = Util.bomAwareCharset(source, charset());

returnsource.readString(charset);

//不做catch,異常全部丟擲給上層

}finally{//確保原始位元組資料得到處理

Util.closeQuietly(source);//閱後即焚,這樣可以迅速騰出記憶體空間來

}

}

如果一定要攔截出資料內容,我們就不能直接讀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資料的函式peekBody,實現原理也是copy:

//Response原始碼

publicResponseBodypeekBody(longbyteCount)throwsIOException{

BufferedSource source = body.source();

source.request(byteCount);

Buffer copy = source.buffer().clone();

...

returnResponseBody.create(body.contentType(), result.size(), result);

}

參考

深入解析OkHttp3

OkHttp3原始碼分析[綜述]

Okhttp-wiki 之 Interceptors 攔截器

如果您覺得不錯,請別忘了轉發、分享、點贊讓更多的人去學習,在順便給大家推薦一個架構交流群:617434785,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源。相信對於已經工作和遇到技術瓶頸的碼友,在這個群裡會有你需要的內容。

相關文章