Android Kotlin 協(xié)程初探
一、 它是什么(協(xié)程 和 Kotlin協(xié)程)
1.1 協(xié)程是什么
維基百科:協(xié)程,英文Coroutine \[k?ru’tin\] (可入廳),是計(jì)算機(jī)程序的一類組件,推廣了協(xié)作式多任務(wù)的子程序,允許執(zhí)行被掛起與被恢復(fù)。
作為Google欽定的Android開發(fā)首選語言Kotlin,協(xié)程并不是 Kotlin 提出來的新概念,目前有協(xié)程概念的編程語言有Lua語言、Python語言、Go語言、C語言等,它只是一種編程思想,不局限于特定的語言。
而每一種編程語言中的協(xié)程的概念及實(shí)現(xiàn)又不完全一樣,本次分享主要講Kotlin協(xié)程。
1.2 Kotlin協(xié)程是什么
Kotlin官網(wǎng):協(xié)程是輕量級(jí)線程
可簡(jiǎn)單理解:一個(gè)線程框架,是全新的處理并發(fā)的方式,也是Android上方便簡(jiǎn)化異步執(zhí)行代碼的方式
類似于 Java:線程池 Android:Handler和AsyncTask,RxJava的Schedulers
注:Kotlin不僅僅是面向JVM平臺(tái)的,還有JS/Native,如果用kotlin來寫前端,那Koltin的協(xié)程就是JS意義上的協(xié)程。如果僅僅JVM 平臺(tái),那確實(shí)應(yīng)該是線程框架。
1.3 進(jìn)程、線程、協(xié)程比較
可通過以下兩張圖理解三者的不同和關(guān)系
Android Kotlin 協(xié)程初探 | 京東物流技術(shù)團(tuán)隊(duì)_User
Android Kotlin 協(xié)程初探 | 京東物流技術(shù)團(tuán)隊(duì)_User_02
二、 為什么選擇它(協(xié)程解決什么問題)
異步場(chǎng)景舉例:
- 第一步:接口獲取當(dāng)前用戶token及用戶信息
- 第二步:將用戶的昵稱展示界面上
- 第三步:然后再通過這個(gè)token獲取當(dāng)前用戶的消息未讀數(shù)
- 第四步:并展示在界面上
2.1 現(xiàn)有方案實(shí)現(xiàn)
apiService.getUserInfo().enqueue(object :Callback<User>{
override fun onResponse(call: Call<User>, response: Response<User>) {
val user = response.body()
tvNickName.text = user?.nickName
apiService.getUnReadMsgCount(user?.token).enqueue(object :Callback<Int>{
override fun onResponse(call: Call<Int>, response: Response<Int>) {
val tvUnReadMsgCount = response.body()
tvMsgCount.text = tvUnReadMsgCount.toString()
}
})
}
})
現(xiàn)有方案如何拿到異步任務(wù)的數(shù)據(jù),得不到就毀掉哈哈哈,就是通過回調(diào)函數(shù)來解決。
若嵌套多了,這種畫風(fēng)是不是有點(diǎn)回調(diào)地獄的感覺,俗稱的「callback hell」
2.2 協(xié)程實(shí)現(xiàn)
mainScope.launch {
val user = apiService.getUserInfoSuspend() //IO線程請(qǐng)求數(shù)據(jù)
tvNickName.text = user?.nickName //UI線程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //IO線程請(qǐng)求數(shù)據(jù)
tvMsgCount.text = unReadMsgCount.toString() //UI線程更新界面
}
suspend fun getUserInfoSuspend() :User? {
return withContext(Dispatchers.IO){
//模擬網(wǎng)絡(luò)請(qǐng)求耗時(shí)操作
delay(10)
User("asd123", "userName", "nickName")
}
}
suspend fun getUnReadMsgCountSuspend(token:String?) :Int{
return withContext(Dispatchers.IO){
//模擬網(wǎng)絡(luò)請(qǐng)求耗時(shí)操作
delay(10)
10
}
}
Android Kotlin 協(xié)程初探 | 京東物流技術(shù)團(tuán)隊(duì)_Kotlin_03
紅色框框內(nèi)的就是一個(gè)協(xié)程代碼塊。
可以看得出在協(xié)程實(shí)現(xiàn)中告別了callback,所以再也不會(huì)出現(xiàn)回調(diào)地獄這種情況了,協(xié)程解決了回調(diào)地獄
Android Kotlin 協(xié)程初探 | 京東物流技術(shù)團(tuán)隊(duì)_Kotlin_04
協(xié)程可以讓我們用同步的代碼寫出異步的效果,這也是協(xié)程最大的優(yōu)勢(shì),異步代碼同步去寫。
小結(jié):協(xié)程可以異步代碼同步去寫,解決回調(diào)地獄,讓程序員更方便地處理異步業(yè)務(wù),更方便地切線程,保證主線程安全。
它是怎么做到的?
三、 它是怎么工作的(協(xié)程的原理淺析)
3.1 協(xié)程的掛起和恢復(fù)
掛起(非阻塞式掛起)
suspend 關(guān)鍵字,它是協(xié)程中核心的關(guān)鍵字,是掛起的標(biāo)識(shí)。
下面看一下上述示例代碼切換線程的過程:
Android Kotlin 協(xié)程初探 | 京東物流技術(shù)團(tuán)隊(duì)_狀態(tài)機(jī)_05
每一次從主線程切到IO線程都是一次協(xié)程的掛起操作;
每一次從IO線程切換主線程都是一次協(xié)程的恢復(fù)操作;
掛起和恢復(fù)是suspend函數(shù)特有的能力,其他函數(shù)不具備,掛起的內(nèi)容是協(xié)程,不是掛起線程,也不是掛起函數(shù),當(dāng)線程執(zhí)行到suspend函數(shù)的地方,不會(huì)繼續(xù)執(zhí)行當(dāng)前協(xié)程的代碼了,所以它不會(huì)阻塞線程,是非阻塞式掛起。
有掛起必然有恢復(fù)流程, 恢復(fù)是指將已經(jīng)被掛起的目標(biāo)協(xié)程從掛起之處開始恢復(fù)執(zhí)行。在協(xié)程中,掛起和恢復(fù)都不需要我們手動(dòng)處理,這些都是kotlin協(xié)程幫我們自動(dòng)完成的。
那Kotlin協(xié)程是如何幫我們自動(dòng)實(shí)現(xiàn)掛起和恢復(fù)操作的呢?
它是通過Continuation來實(shí)現(xiàn)的。 \[k?n?t?nju?e??(?)n\] (繼續(xù);延續(xù);連續(xù)性;后續(xù)部分)
3.2 協(xié)程的掛起和恢復(fù)的工作原理(Continuation)
CPS + 狀態(tài)機(jī)
Java中沒有suspend函數(shù),suspend是Kotlin中特有的關(guān)鍵字,當(dāng)編譯時(shí),Kotlin編譯器會(huì)將含有suspend關(guān)鍵字的函數(shù)進(jìn)行一次轉(zhuǎn)換。
這種被編譯器轉(zhuǎn)換在kotlin中叫CPS轉(zhuǎn)換(cotinuation-passing-style)。
轉(zhuǎn)換流程如下所示
程序員寫的掛起函數(shù)代碼:
suspend fun getUserInfo() : User {
val user = User("asd123", "userName", "nickName")
return user
}
假想的一種中間態(tài)代碼(便于理解):
fun getUserInfo(callback: Callback<User>): Any? {
val user = User("asd123", "userName", "nickName")
callback.onSuccess(user)
return Unit
}
轉(zhuǎn)換后的代碼:
fun getUserInfo(cont: Continuation<User>): Any? {
val user = User("asd123", "userName", "nickName")
cont.resume(user)
return Unit
}
我們通過Kotlin生成字節(jié)碼工具查看字節(jié)碼,然后將其反編譯成Java代碼:
@Nullable
public final Object getUserInfo(@NotNull Continuation $completion) {
User user = new User("asd123", "userName", "nickName");
return user;
}
這也驗(yàn)證了確實(shí)是會(huì)通過引入一個(gè)Continuation對(duì)象來實(shí)現(xiàn)恢復(fù)的流程,這里的這個(gè)Continuation對(duì)象中包含了Callback的形態(tài)。
它有兩個(gè)作用:1\. 暫停并記住執(zhí)行點(diǎn)位;2. 記住函數(shù)暫停時(shí)刻的局部變量上下文。
所以為什么我們可以用同步的方式寫異步代碼,是因?yàn)镃ontinuation幫我們做了回調(diào)的流程。
下面看一下這個(gè)Continuation 的源碼部分
Android Kotlin 協(xié)程初探 | 京東物流技術(shù)團(tuán)隊(duì)_User_06
可以看到這個(gè)Continuation中封裝了一個(gè)resumeWith的方法,這個(gè)方法就是恢復(fù)用的。
internal abstract class BaseContinuationImpl() : Continuation<Any?> {
public final override fun resumeWith(result: Result<Any?>) {
//省略好多代碼
invokeSuspend()
//省略好多代碼
}
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}
internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
//invokeSuspend() 這個(gè)方法是恢復(fù)的關(guān)鍵一步
繼續(xù)看上述例子:
這是一個(gè)CPS之前的代碼:
suspend fun testCoroutine() {
val user = apiService.getUserInfoSuspend() //掛起函數(shù) IO線程
tvNickName.text = user?.nickName //UI線程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //掛起函數(shù) IO線程
tvMsgCount.text = unReadMsgCount.toString() //UI線程更新界面
}
當(dāng)前掛起函數(shù)里有兩個(gè)掛起函數(shù)
通過kotlin編譯器編譯后:
fun testCoroutine(completion: Continuation<Any?>): Any? {
// TestContinuation本質(zhì)上是匿名內(nèi)部類
class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示協(xié)程狀態(tài)機(jī)當(dāng)前的狀態(tài)
var label: Int = 0
// 兩個(gè)變量,對(duì)應(yīng)原函數(shù)的2個(gè)變量
lateinit var user: Any
lateinit var unReadMsgCount: Int
// result 接收協(xié)程的運(yùn)行結(jié)果
var result = continuation.result
// suspendReturn 接收掛起函數(shù)的返回值
var suspendReturn: Any? = null
// CoroutineSingletons 是個(gè)枚舉類
// COROUTINE_SUSPENDED 代表當(dāng)前函數(shù)被掛起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED
// invokeSuspend 是協(xié)程的關(guān)鍵
// 它最終會(huì)調(diào)用 testCoroutine(this) 開啟協(xié)程狀態(tài)機(jī)
// 狀態(tài)機(jī)相關(guān)代碼就是后面的 when 語句
// 協(xié)程的本質(zhì),可以說就是 CPS + 狀態(tài)機(jī)
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
}
// ...
val continuation = if (completion is TestContinuation) {
completion
} else {
// 作為參數(shù)
// ↓
TestContinuation(completion)
loop = true
while(loop) {
when (continuation.label) {
0 -> {
// 檢測(cè)異常
throwOnFailure(result)
// 將 label 置為 1,準(zhǔn)備進(jìn)入下一次狀態(tài)
continuation.label = 1
// 執(zhí)行 getUserInfoSuspend(第一個(gè)掛起函數(shù))
suspendReturn = getUserInfoSuspend(continuation)
// 判斷是否掛起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}
1 -> {
throwOnFailure(result)
// 獲取 user 值
user = result as Any
// 準(zhǔn)備進(jìn)入下一個(gè)狀態(tài)
continuation.label = 2
// 執(zhí)行 getUnReadMsgCountSuspend
suspendReturn = getUnReadMsgCountSuspend(user.token, continuation)
// 判斷是否掛起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}
2 -> {
throwOnFailure(result)
user = continuation.mUser as Any
unReadMsgCount = continuation.unReadMsgCount as Int
loop = false
}
}
通過一個(gè)label標(biāo)簽控制分支代碼執(zhí)行,label為0,首先會(huì)進(jìn)入第一個(gè)分支,首先將label設(shè)置為下一個(gè)分支的數(shù)值,然后執(zhí)行第一個(gè)suspend方法并傳遞當(dāng)前Continuation,得到返回值,如果是COROUTINE SUSPENDED,協(xié)程框架就直接return,協(xié)程掛起,當(dāng)?shù)谝粋€(gè)suspend方法執(zhí)行完成,會(huì)回調(diào)Continuation的invokeSuspend方法,進(jìn)入第二個(gè)分支執(zhí)行,以此類推執(zhí)行完所有suspend方法。
每一個(gè)掛起點(diǎn)和初始掛起點(diǎn)對(duì)應(yīng)的 Continuation 都會(huì)轉(zhuǎn)化為一種狀態(tài),協(xié)程恢復(fù)只是跳轉(zhuǎn)到下一種狀態(tài)中。掛起函數(shù)將執(zhí)行過程分為多個(gè) Continuation 片段,并且利用狀態(tài)機(jī)的方式保證各個(gè)片段是順序執(zhí)行的。
小結(jié):協(xié)程的掛起和恢復(fù)的本質(zhì)是CPS + 狀態(tài)機(jī)
四、 總結(jié)
總結(jié)幾個(gè)不用協(xié)程實(shí)現(xiàn)起來很麻煩的騷操作:
- 如果有一個(gè)函數(shù),它的返回值需要等到多個(gè)耗時(shí)的異步任務(wù)都執(zhí)行完畢返回之后,組合所有任務(wù)的返回值作為 最終返回值
- 如果有一個(gè)函數(shù),需要順序執(zhí)行多個(gè)網(wǎng)絡(luò)請(qǐng)求,并且后一個(gè)請(qǐng)求依賴前一個(gè)請(qǐng)求的執(zhí)行結(jié)果
- 當(dāng)前正在執(zhí)行一項(xiàng)異步任務(wù),但是你突然不想要它執(zhí)行了,隨時(shí)可以取消
- 如果你想讓一個(gè)任務(wù)最多執(zhí)行3秒,超過3秒則自動(dòng)取消
Kotlin協(xié)程之所以被認(rèn)為是假協(xié)程,是因?yàn)樗⒉辉谕粋€(gè)線程運(yùn)行,而是真的會(huì)創(chuàng)建多個(gè)線程。
Kotlin協(xié)程在Android上只是一個(gè)類似線程池的封裝,真就是一個(gè)線程框架。但是它卻可以讓我們用同步的代碼風(fēng)格寫出異步的效果,至于怎么做的,這個(gè)不需要我們操心,這些都是kotlin幫我們處理好了,我們需要關(guān)心的是怎么用好它
它就是一個(gè)線程框架。