前言
用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的設計和實現非常精妙,感興趣的可以深入了解一下。
http://www.lxweimin.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 攔截器
如果您覺得不錯,請別忘了轉發、分享、點贊讓更多的人去學習,在順便給大家推薦一個架構交流群:617434785,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源。相信對于已經工作和遇到技術瓶頸的碼友,在這個群里會有你需要的內容。