從設計模式角度看OkHttp原始碼
阿新 • • 發佈:2021-03-16
## 前言
說到原始碼,很多朋友都覺得複雜,難理解。
但是,如果是一個結構清晰且完全解耦的優質原始碼庫呢?
`OkHttp`就是這樣一個存在,對於這個原生網路框架,想必大家也看過很多很多相關的原始碼解析了。
它的原始碼好看,易讀,清晰,所以今天我準備從設計模式的角度再來讀一遍 `OkHttp`的原始碼。
主要內容就分為兩類:
* okhttp的基本運作流程
* 涉及到的設計模式
(本文原始碼版本為okhttp:4.9.0,攔截器會放到下期再講)
## 使用
讀原始碼,首先就要從它的使用方法開始:
```kotlin
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`。
```kotlin
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
```
在這裡,我們例項化了一個HTTP的客戶端client,然後配置了它的一些引數,比如`攔截器、超時時間`。
這種我們通過一個統一的物件,呼叫一個介面或方法,就能完成我們的需求,而起內部的各種複雜物件的呼叫和跳轉都不需要我們關心的設計模式就是`外觀模式(門面模式)`。
>外觀模式(Facade Pattern)隱藏系統的複雜性,並向客戶端提供了一個客戶端可以訪問系統的介面。這種型別的設計模式屬於結構型模式,它向現有的系統新增一個介面,來隱藏系統的複雜性。
其重點就在於系統內部和各個子系統之間的複雜關係我們不需要了解,只需要去差遣這個`門面` 就可以了,在這裡也就是`OkHttpClient`。
它的存在就像一個接待員,我們告訴它我們的需求,要做的事情。然後接待員去內部處理,各種排程,最終完成。
`外觀模式`主要解決的就是降低訪問複雜系統的內部子系統時的複雜度,簡化客戶端與之的介面。
這個模式也是三方庫很常用的設計模式,給你一個物件,你只需要對這個物件使喚,就可以完成需求。
當然,這裡還有一個比較明顯的設計模式是`建造者模式`,下面會說到。
## Request
```kotlin
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`方法的介面。
```kotlin
//使用
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`方法:
```kotlin
override fun enqueue(responseCallback: Callback) {
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
```
再轉向dispatcher。
```kotlin
//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)
```kotlin
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`方法:
```kotlin
override fun execute(): Response {
//...
return getResponseWithInterceptorChain()
}
```
果然,通過`execute`方法就直接返回了`getResponseWithInterceptorChain`,也就是響應報文。
到這裡,`okhttp`的大體流程就結束了,這部分的流程大概就是:
設定請求報文 -> 配置客戶端引數 -> 根據同步或非同步判斷是否用子執行緒 -> 發起請求並獲取響應報文 -> 通過Callback介面回撥結果
剩下的內容就全部在`getResponseWithInterceptorChain`方法中,這也就是okhttp的核心。
## getResponseWithInterceptorChain
```kotlin
internal fun getResponseWithInterceptorChain(): Response {
// Build a full stack of interceptors.
val interceptors = mutableListOf