[TOC]
# 協程
## 什么是協程?
**`Kotlin`協程的核心競爭力在于:它能簡化異步并發任務,以同步方式寫異步代碼**
這也是為什么要引入協程的原因了:簡化異步并發任務
## 協程與線程的區別是什么?
協程基于線程,但相對于線程輕量很多,可理解為在用戶層模擬線程操作;
每創建一個協程,都有一個內核態線程動態綁定,用戶態下實現調度、切換,真正執行任務的還是內核線程。
線程的上下文切換都需要內核參與,而協程的上下文切換,完全由用戶去控制,避免了大量的中斷參與,減少了線程上下文切換與調度消耗的資源。
線程是操作系統層面的概念,協程是語言層面的概念
**線程與協程最大的區別在于:線程是被動掛起恢復,協程是主動掛起恢復**
## `Kotlin`中的協程是什么?
"假"協程,`Kotlin`在語言級別并沒有實現一種同步機制(鎖),還是依靠`Kotlin-JVM`的提供的`Java`關鍵字(如`synchronized`),即鎖的實現還是交給線程處理
因而`Kotlin`協程本質上只是一套基于原生`Java線程池` 的封裝。
`Kotlin` 協程的核心競爭力在于:它能簡化異步并發任務,以同步方式寫異步代碼。
# 協程要點 suspend
上面的代碼之所以能寫成類似`同步`的方式,關鍵還是在于那三個請求函數的定義。與普通函數不同的地方在于,它們都被 `suspend` 修飾,這代表它們都是:`掛起函數`。
~~~kotlin
// delay(1000L)用于模擬網絡請求
//掛起函數
// ↓
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}
//掛起函數
// ↓
suspend fun getFriendList(user: String): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "Tom, Jack"
}
//掛起函數
// ↓
suspend fun getFeedList(list: String): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "{FeedList..}"
}
復制代碼
~~~
那么,掛起函數到底是什么?
## 掛起函數
掛起函數(Suspending Function),從字面上理解,就是`可以被掛起的函數`。suspend 有:掛起,`暫停`的意思。在這個語境下,也有點暫停的意思。暫停更容易被理解,但掛起更準確。
掛起函數,能被**掛起**,當然也能**恢復**,他們一般是成對出現的。
我們來看看掛起函數的執行流程,注意動畫當中出現的`閃爍`,這代表正在請求網絡。
**一定要多看幾遍,確保沒有遺漏其中的細節。**

從上面的動畫,我們能知道:
* 表面上看起來是同步的代碼,實際上也涉及到了線程切換。
* 一行代碼,切換了兩個線程。
* `=`左邊:主線程
* `=`右邊:IO線程
* 每一次從`主線程`到`IO線程`,都是一次協程`掛起`(suspend)
* 每一次從`IO線程`到`主線程`,都是一次協程`恢復`(resume)。
* 掛起和恢復,這是掛起函數特有的能力,普通函數是不具備的。
* 掛起,只是將程序執行流程轉移到了其他線程,主線程并未被阻塞。
* 如果以上代碼運行在 Android 系統,我們的 App 是仍然可以響應用戶的操作的,主線程并不繁忙,這也很容易理解。
掛起函數的執行流程我們已經很清楚了,那么,Kotlin 協程到底是如何做到`一行代碼切換兩個線程`的?
這一切的`魔法`都藏在了掛起函數的`suspend`關鍵字里。
# suspend原理
`CPS`與`狀態機`就是協程實現的核心
1. 增加了`Continuation`類型的參數 (callback 返回結果)
2. 返回類型從`String`轉變成了`Any`(返回是否被掛起)
3. `continuation.label` 是狀態流轉的關鍵,`label`改變一次代表協程發生了一次掛起恢復
4. 我們寫在協程里的代碼,被拆分到狀態機里各個狀態中,分開執行
## CPS 轉化
下面用動畫演示掛起函數在 `CPS` 轉換過程中,函數簽名的變化:
 可以看出主要有兩點變化
1.增加了`Continuation`類型的參數
2.返回類型從`String`轉變成了`Any`
參數的變化我們之前講過,為什么返回值要變呢?
### 掛起函數返回值
掛起函數經過 `CPS` 轉換后,它的返回值有一個重要作用:標志該掛起函數有沒有被掛起。
聽起來有點奇怪,掛起函數還會不掛起嗎?
> 只要被`suspend`修飾的函數都是掛起函數,但是不是所有掛起函數都會被掛起
> 只有當掛起函數里包含異步操作時,它才會被真正掛起
由于 `suspend` 修飾的函數,既可能返回 `CoroutineSingletons.COROUTINE_SUSPENDED`,表示掛起
也可能返回同步運行的結果,甚至可能返回 null
為了適配所有的可能性,`CPS` 轉換后的函數返回值類型就只能是 `Any?`了。
## 狀態機
`kotlin`協程的實現依賴于狀態機
想要查看其實現,可以將`kotin`源碼反編譯成字節碼來查看編譯后的代碼
關于字節碼的分析之前已經有很多人做過了,而且做的很好,可參考:[Kotlin Jetpack 實戰 | 09. 圖解協程原理](https://juejin.cn/post/6883652600462327821#heading-14 "https://juejin.cn/post/6883652600462327821#heading-14")
讀者可通過上面的鏈接進行詳細的學習,下面給出狀態機的動畫演示

1. 協程實現的核心就是`CPS`變換與狀態機
2. 協程執行到掛起函數,一個函數如果被掛起了,它的返回值會是:`CoroutineSingletons.COROUTINE_SUSPENDED`
3. 掛起函數執行完成后,通過`Continuation.resume`方法回調,這里的`Continuation`是通過`CPS`傳入的
4. 傳入的`Continuation`實際上是`ContinuationImpl`,`resume`方法最后會再次回到`invokeSuspend`方法中
5. `invokeSuspend`方法即是我們寫的代碼執行的地方,在協程運行過程中會執行多次
6. `invokeSuspend`中通過狀態機實現狀態的流轉
7. `continuation.label` 是狀態流轉的關鍵,`label`改變一次代表協程發生了一次掛起恢復
8. 通過`break label`實現`goTo`的跳轉效果
9. 我們寫在協程里的代碼,被拆分到狀態機里各個狀態中,分開執行
10. 每次協程切換后,都會檢查是否發生異常
11. 切換協程之前,狀態機會把之前的結果以成員變量的方式保存在 `continuation` 中。
以上是狀態機流轉的大概流程,讀者可跟著參考鏈接,過一下編譯后的字節碼執行流程后,再來判斷這個流程是否正確
# 協程怎么進行線程切換
簡單來講主要包括以下步驟:
1.向`CoroutineContext`添加`Dispatcher`,指定運行的協程
2.在啟動時將`suspend block`創建成`Continuation`,并調用`intercepted`生成`DispatchedContinuation`
3.`DispatchedContinuation`就是對原有協程的裝飾,在這里調用`Dispatcher`完成線程切換任務后,`resume`被裝飾的協程,就會執行協程體內的代碼了
**其實`kotlin`協程就是用裝飾器模式實現線程切換的**
# Flow
`Flow` 就是 `Kotlin` 協程與響應式編程模型結合的產物,你會發現它與 `RxJava` 非常像,二者之間也有相互轉換的 `API`,使用起來非常方便。
`Flow`有以下特點:
1.冷數據流,不消費則不生產,這一點與`Channel`正相反:`Channel`的發送端并不依賴于接收端。
2.`Flow`通過`flowOn`改變數據發射的線程,數據消費線程則由協程所在線程決定
3.與`RxJava`類似,支持通過`catch`捕獲異常,通過`onCompletion` 回調完成
4.`Flow`沒有提供取消方法,可以通過取消`Flow`所在協程的方式來取消
## `Flow`為什么是個冷流?
冷流即開始消費時才生產數據,不消費則不生產,我們來看下源碼
先看下`flow{}`中發生了什么
~~~kotlin
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)
// Named anonymous object
private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() {
override suspend fun collectSafely(collector: FlowCollector<T>) {
collector.block()
}
}
復制代碼
~~~
可以看出,`flow{}`中做的事也很簡單,主要就是創建了一個繼承自`AbstractFlow`的`SafeFlow`
再來看下`AbstractFlow`中的內容
~~~kotlin
public abstract class AbstractFlow<T> : Flow<T> {
@InternalCoroutinesApi
public final override suspend fun collect(collector: FlowCollector<T>) {
// 1. collector 做一層包裝
val safeCollector = SafeCollector(collector, coroutineContext)
try {
// 2. 處理數據接收者
collectSafely(safeCollector)
} finally {
// 3. 釋放協程相關的參數
safeCollector.releaseIntercepted()
}
}
// collectSafely 方法應當遵循以下的約束
// 1. 不應當在collectSafely方法里面切換線程,比如 withContext(Dispatchers.IO)
// 2. collectSafely 默認不是線程安全的
public abstract suspend fun collectSafely(collector: FlowCollector<T>)
}
private class SafeFlow<T>(private val block: suspend FlowCollector<T>.() -> Unit) : AbstractFlow<T>() {
override suspend fun collectSafely(collector: FlowCollector<T>) {
collector.block()
}
}
復制代碼
~~~
發現主要做了三件事:
1.對數據接收方`FlowCollector` 做了一層包裝,也就是這個`SafeCollector`
2.調用它里面的抽象方法`AbstractFlow#collectSafely` 方法。
3.釋放協程的一些信息。
結合以下之前看的`SafeFlow`,它實現了`AbstractFlow#collectSafely`方法,調用了`collector.block()`,也就是運行了`flow{}`塊中的代碼。
現在就很清晰了,為什么`Flow`是冷流?
**因為它會在每一次`collect`的時候才會去觸發發送數據的動作**
## `Flow`是怎么切換線程的
`Flow`切換線程的方式與協程切換線程是類似的
都是通過啟動一個子協程,然后通過`CoroutineContext`中的`Dispatchers`切換線程
不同的地方在于`Flow`切換過程中利用了`Channel`來傳遞數據

由于`Flow`切換線程的源碼過多,就不在這里綴述了,有興趣的同學可以跟一下源碼,詳情可見:[flowOn()如何做到切換協程](https://juejin.cn/post/6914802148614242312#heading-9 "https://juejin.cn/post/6914802148614242312#heading-9")
# 協程異常處理
## CoroutineExceptionHandler
* “ CoroutineExceptionHandler是用于全局“全部捕獲”行為的最后手段。 您無法從CoroutineExceptionHandler中的異常中恢復。 當調用處理程序時,協程已經完成,并帶有相應的異常。 通常,處理程序用于記錄異常,顯示某種錯誤消息,終止和/或重新啟動應用程序。
* 為了使CoroutineExceptionHandler起作用,必須將其設置在CoroutineScope或頂級協程中。
* 如果需要在代碼的特定部分處理異常,建議在協程內部的相應代碼周圍使用try / catch。 這樣,您可以防止協程異常完成(現在已捕獲異常),重試該操作和/或采取其他任意操作:
# 異常的傳播機制
本文主要分析了`kotlin`協程的異常傳播機制,主要分為以下幾步
1. 協程體內拋出異常
2. 判斷是否是`CancellationException`,如果是則不做處理
3. 判斷父協程是否為空或為`supervisorScope`,如果是則調用`handleJobException`,處理異常
4. 如果不是則將異常傳遞給父協程,然后父協程再進行一遍上面的流程
以上步驟總結為流程圖如下所示:

# 參考資料
[全民 Kotlin:協程特別篇](https://mp.weixin.qq.com/s/xqAdliU4g0cV1oIwwwYJlA)
[【帶著問題學】協程到底是什么?](https://juejin.cn/post/6973650934664527885)
[Kotlin Jetpack 實戰 | 09. 圖解協程原理](https://juejin.cn/post/6883652600462327821)
[協程異常機制與優雅封裝 | 技術點評](https://juejin.cn/post/6935472332735512606)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺