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

前言json

用OkHttp好久了,也看了不少人寫的源碼分析,在這裏結合本身的感悟,記錄一下對OkHttp源碼理解的幾點心得。設計模式

總體結構數組

網絡請求框架雖然都要作請求任務的封裝和管理,可是最大的難點在於網絡請求任務的多樣性,由於網絡層狀況複雜,不只要考慮功能性的創建Socket鏈接、文件流傳輸、TLS安全、多平臺等,還要考慮性能上的Cache複用、Cache過時、鏈接池複用等,這些功能若是交錯在一塊兒,實現和維護都會有很大的問題。緩存

爲了解決這個問題,OkHttp採用了分層設計的思想,使用多層攔截器,每一個攔截器解決一個問題,多層攔截器套在一塊兒,就像設計模式中的裝飾者模式同樣,能夠在保證每層功能高內聚的狀況下,解決多樣性的問題。安全

OkHttp使用了外觀模式,開發者直接操做的主要就是OkHttpClient,其實若是粗略劃分的話,整個OkHttp框架從功能上能夠分爲三部分:服務器

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

2.分發器及線程池:具體的類就是Dispatcher、ThreadPoolExecutor等。網絡

3.攔截器:實現了分層設計+鏈式調用,具體的類就是Interceptor+RealInterceptorChain。多線程

至於更具體的操做,均由攔截器實現,包括應用層攔截器、網絡層攔截器等,開發者也能夠本身擴展新的攔截器。app

請求

網絡請求其實能夠分爲數據和行爲兩部分,數據即咱們的請求數據和返回數據,行爲則是發起網絡請求,以及獲得處理結果。

數據(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的設計和實現很是精妙,感興趣的能夠深刻了解一下。

https://www.jianshu.com/p/132733115f95

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 攔截器

若是您以爲不錯,請別忘了轉發、分享、點贊讓更多的人去學習

相關文章
相關標籤/搜索