從設計模式角度看OkHttp源碼
- 2021 年 3 月 16 日
- 筆記
前言
說到源碼,很多朋友都覺得複雜,難理解。
但是,如果是一個結構清晰且完全解耦的優質源碼庫呢?
OkHttp就是這樣一個存在,對於這個原生網路框架,想必大家也看過很多很多相關的源碼解析了。
它的源碼好看,易讀,清晰,所以今天我準備從設計模式的角度再來讀一遍 OkHttp的源碼。
主要內容就分為兩類:
- okhttp的基本運作流程
- 涉及到的設計模式
(本文源碼版本為okhttp:4.9.0,攔截器會放到下期再講)
使用
讀源碼,首先就要從它的使用方法開始:
val okHttpClient = OkHttpClient()
val request: Request = Request.Builder()
.url(url)
.build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(TAG, "onFailure: ")
}
override fun onResponse(call: Call, response: Response) {
Log.d(TAG, "onResponse: " + response.body?.string())
}
})
從這個使用方法來看,我抽出了四個重要資訊:
- okHttpClient
- Request
- newCall(request)
- enqueue(Callback)
大體意思我們可以先猜猜看:
配置一個客戶端實例okHttpClient和一個Request請求,然後這個請求通過okHttpClient的newCall方法封裝,最後用enqueue方法發送出去,並收到Callback響應。
接下來就一個個去認證,並找找其中的設計模式。
okHttpClient
首先看看這個okhttp的客戶端對象,也就是okHttpClient。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
在這裡,我們實例化了一個HTTP的客戶端client,然後配置了它的一些參數,比如攔截器、超時時間。
這種我們通過一個統一的對象,調用一個介面或方法,就能完成我們的需求,而起內部的各種複雜對象的調用和跳轉都不需要我們關心的設計模式就是外觀模式(門面模式)。
外觀模式(Facade Pattern)隱藏系統的複雜性,並向客戶端提供了一個客戶端可以訪問系統的介面。這種類型的設計模式屬於結構型模式,它向現有的系統添加一個介面,來隱藏系統的複雜性。
其重點就在於系統內部和各個子系統之間的複雜關係我們不需要了解,只需要去差遣這個門面 就可以了,在這裡也就是OkHttpClient。
它的存在就像一個接待員,我們告訴它我們的需求,要做的事情。然後接待員去內部處理,各種調度,最終完成。
外觀模式主要解決的就是降低訪問複雜系統的內部子系統時的複雜度,簡化客戶端與之的介面。
這個模式也是三方庫很常用的設計模式,給你一個對象,你只需要對這個對象使喚,就可以完成需求。
當然,這裡還有一個比較明顯的設計模式是建造者模式,下面會說到。
Request
val request: Request = Request.Builder()
.url(url)
.build()
//Request.kt
open class Builder {
internal var url: HttpUrl? = null
internal var method: String
internal var headers: Headers.Builder
internal var body: RequestBody? = null
constructor() {
this.method = "GET"
this.headers = Headers.Builder()
}
open fun build(): Request {
return Request(
checkNotNull(url) { "url == null" },
method,
headers.build(),
body,
tags.toImmutableMap()
)
}
}
從Request的生成程式碼中可以看到,用到了其內部類Builder,然後通過Builder類組裝出了一個完整的有著各種參數的Request類。
這也就是典型的 建造者(Builder)模式 。
建造者(Builder)模式,將一個複雜的對象的構建與它的表示分離,是的同樣的構建過程可以創建不同的表示。
我們可以通過Builder,構建了不同的Request請求,只需要傳入不同的請求地址url,請求方法method,頭部資訊headers,請求體body即可。
(這也就是網路請求中的請求報文的格式)
這種可以通過構建形成不同的表示的 設計模式 就是 建造者模式,也是用的很多,主要為了方便我們傳入不同的參數進行構建對象。
又比如上面okHttpClient的構建。
newCall(request)
接下來是調用OkHttpClient類的newCall方法獲取一個可以去調用enqueue方法的介面。
//使用
val okHttpClient = OkHttpClient()
okHttpClient.newCall(request)
//OkHttpClient.kt
open class OkHttpClient internal constructor(builder: Builder) : Cloneable, Call.Factory, WebSocket.Factory {
override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)
}
//Call介面
interface Call : Cloneable {
fun execute(): Response
fun enqueue(responseCallback: Callback)
fun interface Factory {
fun newCall(request: Request): Call
}
}
newCall方法,其實是Call.Factory介面裡面的方法。
也就是創建Call的過程,是通過Call.Factory介面的newCall方法創建的,而真正實現這個方法交給了這個介面的子類OkHttpClient。
那這種定義了統一創建對象的介面,然後由子類來決定實例化這個對象的設計模式就是 工廠模式。
在工廠模式中,我們在創建對象時不會對客戶端暴露創建邏輯,並且是通過使用一個共同的介面來指向新創建的對象。
當然,okhttp這裡的工廠有點小,只有一條生產線,就是Call介面,而且只有一個產品,RealCall。
enqueue(Callback)
接下來這個方法enqueue,肯定就是okhttp源碼的重中之重了,剛才說到newCall方法其實是獲取了RealCall對象,所以就走到了RealCall的enqueue方法:
override fun enqueue(responseCallback: Callback) {
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
再轉向dispatcher。
//Dispatcher.kt
val executorService: ExecutorService
get() {
if (executorServiceOrNull == null) {
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
}
return executorServiceOrNull!!
}
internal fun enqueue(call: AsyncCall) {
promoteAndExecute()
}
private fun promoteAndExecute(): Boolean {
//通過執行緒池切換執行緒
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
asyncCall.executeOn(executorService)
}
return isRunning
}
//RealCall.kt
fun executeOn(executorService: ExecutorService) {
try {
executorService.execute(this)
success = true
}
}
這裡用到了一個新的類Dispatcher,調用到的方法是asyncCall.executeOn(executorService)
這個executorService參數大家應該都熟悉吧,執行緒池。最後是調用executorService.execute方法執行執行緒池任務。
而執行緒池的概念其實也是用到了一種設計模式,叫做享元模式。
享元模式(Flyweight Pattern)主要用於減少創建對象的數量,以減少記憶體佔用和提高性能。這種類型的設計模式屬於結構型模式,它提供了減少對象數量從而改善應用所需的對象結構的方式。
其核心就在於共享對象,所有很多的池類對象,比如執行緒池、連接池等都是採用了享元模式 這一設計模式。當然,okhttp中不止是有執行緒池,還有連接池提供連接復用,管理所有的socket連接。
再回到Dispatcher,所以這個類是幹嘛的呢?就是切換執行緒用的,因為我們調用的enqueue是非同步方法,所以最後會用到執行緒池切換執行緒,執行任務。
繼續看看execute(this)中的this任務。
execute(this)
override fun run() {
threadName("OkHttp ${redactedUrl()}") {
try {
//獲取響應報文,並回調給Callback
val response = getResponseWithInterceptorChain()
responseCallback.onResponse(this@RealCall, response)
} catch (e: IOException) {
if (!signalledCallback) {
responseCallback.onFailure(this@RealCall, e)
}
} catch (t: Throwable) {
cancel()
if (!signalledCallback) {
responseCallback.onFailure(this@RealCall, canceledException)
}
}
}
沒錯,這裡就是請求介面的地方了,通過getResponseWithInterceptorChain方法獲取響應報文response,然後通過Callback的onResponse方法回調,或者是有異常就通過onFailure方法回調。
那同步方法是不是就沒用到執行緒池呢?去找找execute方法:
override fun execute(): Response {
//...
return getResponseWithInterceptorChain()
}
果然,通過execute方法就直接返回了getResponseWithInterceptorChain,也就是響應報文。
到這裡,okhttp的大體流程就結束了,這部分的流程大概就是:
設置請求報文 -> 配置客戶端參數 -> 根據同步或非同步判斷是否用子執行緒 -> 發起請求並獲取響應報文 -> 通過Callback介面回調結果
剩下的內容就全部在getResponseWithInterceptorChain方法中,這也就是okhttp的核心。
getResponseWithInterceptorChain
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
val chain = RealInterceptorChain(
interceptors = interceptors
//...
)
val response = chain.proceed(originalRequest)
}
程式碼不是很複雜,就是 加加加 攔截器,然後組裝成一個chain類,調用proceed方法,得到響應報文response。
override fun proceed(request: Request): Response {
//找到下一個攔截器
val next = copy(index = index + 1, request = request)
val interceptor = interceptors[index]
val response = interceptor.intercept(next)
return response
}
簡化了下程式碼,主要邏輯就是獲取下一個攔截器(index+1),然後調用攔截器的intercept方法。
然後在攔截器裡面的程式碼統一都是這種格式:
override fun intercept(chain: Interceptor.Chain): Response {
//做事情A
response = realChain.proceed(request)
//做事情B
}
結合兩段程式碼,會形成一條鏈,這條鏈組織了所有連接器的工作。類似這樣:
攔截器1做事情A -> 攔截器2做事情A -> 攔截器3做事情A -> 攔截器3做事情B -> 攔截器2做事情B -> 攔截器1做事情B
應該是好理解的吧,通過proceed方法把每個攔截器連接起來了。
而最後一個攔截器ConnectInterceptor就是分割事情A和事情B,其作用就是進行真正的與伺服器的通訊,向伺服器發送數據,解析讀取的響應數據。
所以事情A和事情B是什麼意思呢?其實就代表了通訊之前的事情和通訊之後的事情。
再來個動畫:

這種思想是不是有點像..遞歸?沒錯,就是遞歸,先遞進執行事情A,再回歸做事情B。
而這種遞歸循環,其實也就是用到了設計模式中的 責任鏈模式。
責任鏈模式(Chain of Responsibility Pattern)為請求創建了一個接收者對象的鏈。這種模式給予請求的類型,對請求的發送者和接收者進行解耦。
簡單的說,就是讓每個對象都能有機會處理這個請求,然後各自完成自己的事情,一直到事件被處理。Android中的事件分發機制也是用到了這種設計模式。
接下來就是了解每個攔截器到底做了什麼事,就可以了解到okhttp的整個流程了,這就是下期的內容了。
先預告一波:
addInterceptor(Interceptor),這是由開發者設置的,會按照開發者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共參數,Header都可以在這裡添加。RetryAndFollowUpInterceptor,這裡會對連接做一些初始化工作,以及請求失敗的重試工作,重定向的後續請求工作。BridgeInterceptor,這裡會為用戶構建一個能夠進行網路訪問的請求,同時後續工作將網路請求回來的響應Response轉化為用戶可用的Response,比如添加文件類型,content-length計算添加,gzip解包。CacheInterceptor,這裡主要是處理cache相關處理,會根據OkHttpClient對象的配置以及快取策略對請求值進行快取,而且如果本地有了可⽤的Cache,就可以在沒有網路交互的情況下就返回快取結果。ConnectInterceptor,這裡主要就是負責建立連接了,會建立TCP連接或者TLS連接,以及負責編碼解碼的HttpCodec。networkInterceptors,這裡也是開發者自己設置的,所以本質上和第一個攔截器差不多,但是由於位置不同,用處也不同。這個位置添加的攔截器可以看到請求和響應的數據了,所以可以做一些網路調試。CallServerInterceptor,這裡就是進行網路數據的請求和響應了,也就是實際的網路I/O操作,通過socket讀寫數據。
總結
讀完okhttp的源碼,感覺就一個字:舒服。
一份好的程式碼應該就是這樣,各模組之間通過各種設計模式進行解耦,閱讀者可以每個模組分別去去閱讀了解,而不是各個模組纏綿在一起,雜亂無章。
最後再總結下okhttp中涉及到的設計模式:
外觀模式。通過okHttpClient這個外觀去實現內部各種功能。建造者模式。構建不同的Request對象。工廠模式。通過OkHttpClient生產出產品RealCall。享元模式。通過執行緒池、連接池共享對象。責任鏈模式。將不同功能的攔截器形成一個鏈。
其實還是有一些設計模式沒說到的,比如
- websocket相關用到的
觀察者模式。 - Cache集合相關的
迭代器模式。
以後遇到了再做補充吧。
參考
//www.runoob.com/design-pattern/design-pattern-tutorial.html
//www.jianshu.com/p/ae2fe5481994
//juejin.cn/post/6895369745445748749
拜拜
感謝大家的閱讀,有一起學習的小夥伴可以關注下我的公眾號——碼上積木❤️❤️
每日一個知識點,積少成多,建立知識體系架構。
這裡有一群很好的Android小夥伴,歡迎大家加入~

