在今年的三月份,我因為需要為項目搭建一個新的網絡請求框架開始接觸 Kotlin 協程。那時我司項目中同時存在著兩種網絡請求方式,采用的技術棧各不相同,Java、Kotlin、RxJava、LiveData 各種混搭,技術棧的不統一長遠來看肯定是會造成很多不便的,所以就打算封裝一個新的網絡請求框架來作為項目的統一規范(前面的人估計也是這么想的,所以就造成了同個項目中的網絡請求方式越來越多 ????),那么就需要考慮采用什么技術棧來實現了
采用 Kotlin 語言來實現必不可少,都這年頭了還用 Java 也說不過去。Retrofit 也必不可少,而目前 Retrofit 也已經支持 Kotlin 協程了,Google 官方推出的 Jetpack 協程擴展庫也越來越多,就最終決定棄用 RxJava 擁抱 Kotlin 協程,將協程作為技術棧之一
當時我是通過翻譯協程官方文檔來作為入門手段,到現在也大半年了,回頭來看感覺官方文檔還是挺晦澀難懂的,就想著再來寫一兩篇入門或進階的文章來加深下理解,希望對你有所幫助
附上我當時翻譯的協程官方文檔:
- Kotlin 協程官方文檔(1)-協程基礎(Coroutine Basics)
- Kotlin 協程官方文檔(2)-取消和超時(Cancellation and Timeouts)
- Kotlin 協程官方文檔(3)-組合掛起函數(Coroutine Context and Dispatchers)
- Kotlin 協程官方文檔(4)-協程上下文和調度器(Coroutine Context and Dispatchers)
- Kotlin 協程官方文檔(5)-異步流(Asynchronous Flow)
- Kotlin 協程官方文檔(6)-通道(Channels)
- Kotlin 協程官方文檔(7)-異常處理(Exception Handling)
- Kotlin 協程官方文檔(8)-共享可變狀態和并發性(Shared mutable state and concurrency)
- Kotlin 協程官方文檔(9)-選擇表達式(實驗階段)(Select Expression (experimental)
一、Kotlin 協程
Kotlin 協程提供了一種全新處理并發的方式,你可以在 Android 平臺上使用它來簡化異步執行的代碼。協程從 Kotlin 1.3 版本開始引入,但這一概念在編程世界誕生的黎明之際就有了,最早使用協程的編程語言可以追溯到 1967 年的 Simula 語言。在過去幾年間,協程這個概念發展勢頭迅猛,現已經被諸多主流編程語言采用,比如 Javascript、C#、Python、Ruby 以及 Go 等。Kotlin 協程是基于來自其他語言的既定概念
Google 官方推薦將 Kotlin 協程作為在 Android 上進行異步編程的解決方案,值得關注的功能點包括:
- 輕量:可以在單個線程上運行多個協程,因為協程支持掛起,不會使正在運行協程的線程阻塞。掛起比阻塞節省內存,且支持多個并行操作
- 內存泄露更少:使用結構化并發機制在一個作用域內執行多個操作
- 內置取消支持:取消功能會自動通過正在運行的協程層次結構傳播
- Jetpack 集成:許多 Jetpack 庫都包含提供全面協程支持的擴展。某些庫還提供自己的協程作用域,可供你用于結構化并發
如果是用于 Android 平臺的話,可以只引用以下的 coroutines-android,當中已經包含了 coroutines-core
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
二、第一個協程
協程可以稱為 輕量級線程。Kotlin 協程在 CoroutineScope 的上下文中通過 launch、async 等 協程構造器(CoroutineBuilder)來聲明并啟動
fun main() {
GlobalScope.launch(context = Dispatchers.IO) {
//延時一秒
delay(1000)
log("launch")
}
//主動休眠兩秒,防止 JVM 過快退出
Thread.sleep(2000)
log("end")
}
private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")
[DefaultDispatcher-worker-1] launch
[main] end
在上面的例子中,通過 GlobalScope(全局作用域)啟動了一個協程,在延遲一秒后輸出一行日志。從輸出結果可以看出來,啟動的協程是運行在協程內部的線程池中。雖然從表現結果上來看,啟動一個協程類似于我們直接使用 Thread 來執行耗時任務,但實際上協程和線程有著本質上的區別。通過使用協程,可以極大的提高線程的并發效率,避免以往的嵌套回調地獄,極大提高了代碼的可讀性
以上代碼就涉及到了協程的四個基礎概念:
- suspend function。即掛起函數,delay() 就是協程庫提供的一個用于實現非阻塞式延時的掛起函數
- CoroutineScope。即協程作用域,GlobalScope 是 CoroutineScope 的一個實現類,用于指定協程的作用范圍,可用于管理多個協程的生命周期,所有協程都需要通過 CoroutineScope 來啟動
- CoroutineContext。即協程上下文,包含多種類型的配置參數。
Dispatchers.IO
就是 CoroutineContext 這個抽象概念的一種實現,用于指定協程的運行載體,即用于指定協程要運行在哪類線程上 - CoroutineBuilder。即協程構建器,協程在 CoroutineScope 的上下文中通過 launch、async 等協程構建器來進行聲明并啟動。launch、async 均被聲明為 CoroutineScope 的擴展方法
三、suspend
如果上述例子試圖直接在 GlobalScope 外調用 delay()
函數的話,IDE 就會提示一個錯誤:Suspend function 'delay' should be called only from a coroutine or another suspend function。意思是:delay()
函數是一個掛起函數,只能由協程或者由其它掛起函數來調用
delay()
函數就使用了 suspend 進行修飾,用 suspend 修飾的函數就是掛起函數
public suspend fun delay(timeMillis: Long)
讀者在網上看關于協程的文章的時候,應該經常會看到這么一句話:掛起函數不會阻塞其所在線程,而是會將協程掛起,在特定的時候才再恢復執行
對于這句話我的理解是:delay()
函數類似于 Java 中的 Thread.sleep()
,而之所以說 delay()
函數是非阻塞的,是因為它和單純的線程休眠有著本質的區別。例如,當在 ThreadA 上運行的 CoroutineA 調用了delay(1000L)
函數指定延遲一秒后再運行,ThreadA 會轉而去執行 CoroutineB,等到一秒后再來繼續執行 CoroutineA。所以,ThreadA 并不會因為 CoroutineA 的延時而阻塞,而是能繼續去執行其它任務,所以掛起函數并不會阻塞其所在線程,這樣就極大地提高了線程的并發靈活度,最大化了線程的利用效率。而如果是使用Thread.sleep()
的話,線程就只能干等著而不能去執行其它任務,降低了線程的利用效率
協程是運行于線程上的,一個線程可以運行多個(幾千上萬個)協程。線程的調度行為是由操作系統來管理的,而協程的調度行為是可以由開發者來指定并由編譯器來實現的,協程能夠細粒度地控制多個任務的執行時機和執行線程,當線程所執行的當前協程被 suspend 后,該線程也可以騰出資源去處理其他任務
四、suspend 掛起與恢復
協程在常規函數的基礎上添加了兩項操作用于處理長時間運行的任務,在invoke
(或 call
)和return
之外,協程添加了suspend
和 resume
:
-
suspend
用于暫停執行當前協程,并保存所有局部變量 -
resume
用于讓已暫停的協程從暫停處繼續執行
suspend 函數只能由其它 suspend 函數調用,或者是由協程來調用
以下示例展示了一項任務(假設 get 方法是一個網絡請求任務)的簡單協程實現:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
在上面的示例中,get()
仍在主線程上被調用,但它會在啟動網絡請求之前暫停協程。get()
主體內通過調用 withContext(Dispatchers.IO)
創建了一個在 IO 線程池中運行的代碼塊,在該塊內的任何代碼都始終通過 IO 調度器執行。當網絡請求完成后,get()
會恢復已暫停的協程,使得主線程協程可以直接拿到網絡請求結果而不用使用回調來通知主線程。Retrofit 就是以這種方式來實現對協程的支持
Kotlin 使用 堆棧幀 來管理要運行哪個函數以及所有局部變量。暫停協程時,系統會復制并保存當前的堆棧幀以供稍后使用。恢復時,會將堆棧幀從其保存的位置復制回來,然后函數再次開始運行。雖然代碼可能看起來像普通的順序阻塞請求,協程也能確保網絡請求不會阻塞主線程
在主線程進行的 暫停協程 和 恢復協程 的兩個操作,既實現了將耗時任務交由后臺線程完成,保障了主線程安全,又以同步代碼的方式完成了實際上的多線程異步調用。可以說,在 Android 平臺上協程主要就用來解決兩個問題:
- 處理耗時任務 (Long running tasks),這種任務常常會阻塞主線程
- 保證主線程安全 (Main-safety),即確保安全地從主線程調用任何 suspend 函數
五、CoroutineScope
CoroutineScope 即 協程作用域,用于對協程進行追蹤。如果我們啟動了多個協程但是沒有一個可以對其進行統一管理的途徑的話,就會導致我們的代碼臃腫雜亂,甚至發生內存泄露或者任務泄露。為了確保所有的協程都會被追蹤,Kotlin 不允許在沒有 CoroutineScope 的情況下啟動協程。CoroutineScope 可被看作是一個具有超能力的 ExecutorService 的輕量級版本。它能啟動協程,同時這個協程還具備上文所說的 suspend 和 resume 的優勢
所有的協程都需要通過 CoroutineScope 來啟動,它會跟蹤通過 launch
或 async
創建的所有協程,你可以隨時調用 scope.cancel()
取消正在運行的協程。CoroutineScope 本身并不運行協程,它只是確保你不會失去對協程的追蹤,即使協程被掛起也是如此。在 Android 中,某些 ktx 庫為某些生命周期類提供了自己的 CoroutineScope,例如,ViewModel 有 viewModelScope
,Lifecycle 有 lifecycleScope
CoroutineScope 大體上可以分為三種:
- GlobalScope。即全局協程作用域,在這個范圍內啟動的協程可以一直運行直到應用停止運行。GlobalScope 本身不會阻塞當前線程,且啟動的協程相當于守護線程,不會阻止 JVM 結束運行
- runBlocking。一個頂層函數,和 GlobalScope 不一樣,它會阻塞當前線程直到其內部所有相同作用域的協程執行結束
- 自定義 CoroutineScope。可用于實現主動控制協程的生命周期范圍,對于 Android 開發來說最大意義之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的對象中按需取消所有協程任務,從而確保生命周期安全,避免內存泄露
1、GlobalScope
GlobalScope 屬于 全局作用域,這意味著通過 GlobalScope 啟動的協程的生命周期只受整個應用程序的生命周期的限制,只要整個應用程序還在運行且協程的任務還未結束,協程就可以一直運行
GlobalScope 不會阻塞其所在線程,所以以下代碼中主線程的日志會早于 GlobalScope 內部輸出日志。此外,GlobalScope 啟動的協程相當于守護線程,不會阻止 JVM 結束運行,所以如果將主線程的休眠時間改為三百毫秒的話,就不會看到 launch A 輸出日志
fun main() {
log("start")
GlobalScope.launch {
launch {
delay(400)
log("launch A")
}
launch {
delay(300)
log("launch B")
}
log("GlobalScope")
}
log("end")
Thread.sleep(500)
}
[main] start
[main] end
[DefaultDispatcher-worker-1] GlobalScope
[DefaultDispatcher-worker-3] launch B
[DefaultDispatcher-worker-3] launch A
GlobalScope.launch
會創建一個頂級協程,盡管它很輕量級,但在運行時還是會消耗一些內存資源,且可以一直運行直到整個應用程序停止(只要任務還未結束),這可能會導致內存泄露,所以在日常開發中應該謹慎使用 GlobalScope
2、runBlocking
也可以使用 runBlocking 這個頂層函數來啟動協程,runBlocking 函數的第二個參數即協程的執行體,該參數被聲明為 CoroutineScope 的擴展函數,因此執行體就包含了一個隱式的 CoroutineScope,所以在 runBlocking 內部可以來直接啟動協程
public fun <T> runBlocking(context: CoroutineContext =
EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
runBlocking 的一個方便之處就是:只有當內部相同作用域的所有協程都運行結束后,聲明在 runBlocking 之后的代碼才能執行,即 runBlocking 會阻塞其所在線程
看以下代碼。runBlocking 內部啟動的兩個協程會各自做耗時操作,從輸出結果可以看出來兩個協程還是在交叉并發執行,且 runBlocking 會等到兩個協程都執行結束后才會退出,外部的日志輸出結果有明確的先后順序。即 runBlocking 內部啟動的協程是非阻塞式的,但 runBlocking 阻塞了其所在線程。此外,runBlocking 只會等待相同作用域的協程完成才會退出,而不會等待 GlobalScope 等其它作用域啟動的協程
fun main() {
log("start")
runBlocking {
launch {
repeat(3) {
delay(100)
log("launchA - $it")
}
}
launch {
repeat(3) {
delay(100)
log("launchB - $it")
}
}
GlobalScope.launch {
repeat(3) {
delay(120)
log("GlobalScope - $it")
}
}
}
log("end")
}
[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end
所以說,runBlocking 本身帶有阻塞線程的意味,但其內部運行的協程又是非阻塞的,讀者需要明白這兩者的區別
基于是否會阻塞線程的區別,以下代碼中 runBlocking 會早于 GlobalScope 輸出日志
fun main() {
GlobalScope.launch(Dispatchers.IO) {
delay(600)
log("GlobalScope")
}
runBlocking {
delay(500)
log("runBlocking")
}
//主動休眠兩百毫秒,使得和 runBlocking 加起來的延遲時間多于六百毫秒
Thread.sleep(200)
log("after sleep")
}
[main] runBlocking
[DefaultDispatcher-worker-1] GlobalScope
[main] after sleep
3、coroutineScope
coroutineScope
函數用于創建一個獨立的協程作用域,直到所有啟動的協程都完成后才結束自身。runBlocking
和 coroutineScope
看起來很像,因為它們都需要等待其內部所有相同作用域的協程結束后才會結束自己。兩者的主要區別在于 runBlocking
方法會阻塞當前線程,而 coroutineScope
不會,而是會掛起并釋放底層線程以供其它協程使用。基于這個差別,runBlocking
是一個普通函數,而 coroutineScope
是一個掛起函數
fun main() = runBlocking {
launch {
delay(100)
log("Task from runBlocking")
}
coroutineScope {
launch {
delay(500)
log("Task from nested launch")
}
delay(50)
log("Task from coroutine scope")
}
log("Coroutine scope is over")
}
[main] Task from coroutine scope
[main] Task from runBlocking
[main] Task from nested launch
[main] Coroutine scope is over
4、supervisorScope
supervisorScope
函數用于創建一個使用了 SupervisorJob 的 coroutineScope,該作用域的特點就是拋出的異常不會連鎖取消同級協程和父協程
fun main() = runBlocking {
launch {
delay(100)
log("Task from runBlocking")
}
supervisorScope {
launch {
delay(500)
log("Task throw Exception")
throw Exception("failed")
}
launch {
delay(600)
log("Task from nested launch")
}
}
log("Coroutine scope is over")
}
[main] Task from runBlocking
[main] Task throw Exception
[main] Task from nested launch
[main] Coroutine scope is over
5、自定義 CoroutineScope
假設我們在 Activity 中先后啟動了多個協程用于執行異步耗時操作,那么當 Activity 退出時,必須取消所有協程以避免內存泄漏。我們可以通過保留每一個 Job 引用然后在 onDestroy
方法里來手動取消,但這種方式相當來說會比較繁瑣和低效。kotlinx.coroutines 提供了 CoroutineScope 來管理多個協程的生命周期
我們可以通過創建與 Activity 生命周期相關聯的協程作用域來管理協程的生命周期。CoroutineScope 的實例可以通過 CoroutineScope()
或 MainScope()
的工廠函數來構建。前者創建通用作用域,后者創建 UI 應用程序的作用域并使用 Dispatchers.Main 作為默認的調度器
class Activity {
private val mainScope = MainScope()
fun onCreate() {
mainScope.launch {
repeat(5) {
delay(1000L * it)
}
}
}
fun onDestroy() {
mainScope.cancel()
}
}
或者,我們可以通過委托模式來讓 Activity 實現 CoroutineScope 接口,從而可以在 Activity 內直接啟動協程而不必顯示地指定它們的上下文,并且在 onDestroy()
中自動取消所有協程
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun onCreate() {
launch {
repeat(5) {
delay(200L * it)
log(it)
}
}
log("Activity Created")
}
fun onDestroy() {
cancel()
log("Activity Destroyed")
}
}
fun main() = runBlocking {
val activity = Activity()
activity.onCreate()
delay(1000)
activity.onDestroy()
delay(1000)
}
從輸出結果可以看出,當回調了onDestroy()
方法后協程就不會再輸出日志了
[main] Activity Created
[DefaultDispatcher-worker-1] 0
[DefaultDispatcher-worker-1] 1
[DefaultDispatcher-worker-1] 2
[main] Activity Destroyed
已取消的作用域無法再創建協程。因此,僅當控制其生命周期的類被銷毀時,才應調用 scope.cancel()
。例如,使用 viewModelScope
時, ViewModel 會在自身的 onCleared()
方法中自動取消作用域
六、CoroutineBuilder
1、launch
看下 launch
函數的方法簽名。launch
是一個作用于 CoroutineScope 的擴展函數,用于在不阻塞當前線程的情況下啟動一個協程,并返回對該協程任務的引用,即 Job 對象
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch
函數共包含三個參數:
- context。用于指定協程的上下文
- start。用于指定協程的啟動方式,默認值為
CoroutineStart.DEFAULT
,即協程會在聲明的同時就立即進入等待調度的狀態,即可以立即執行的狀態。可以通過將其設置為CoroutineStart.LAZY
來實現延遲啟動,即懶加載 - block。用于傳遞協程的執行體,即希望交由協程執行的任務
可以看到 launchA 和 launchB 是并行交叉執行的
fun main() = runBlocking {
val launchA = launch {
repeat(3) {
delay(100)
log("launchA - $it")
}
}
val launchB = launch {
repeat(3) {
delay(100)
log("launchB - $it")
}
}
}
[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2
2、Job
Job 是協程的句柄。使用 launch
或 async
創建的每個協程都會返回一個 Job 實例,該實例唯一標識協程并管理其生命周期。Job 是一個接口類型,這里列舉 Job 幾個比較有用的屬性和函數
//當 Job 處于活動狀態時為 true
//如果 Job 未被取消或沒有失敗,則均處于 active 狀態
public val isActive: Boolean
//當 Job 正常結束或者由于異常結束,均返回 true
public val isCompleted: Boolean
//當 Job 被主動取消或者由于異常結束,均返回 true
public val isCancelled: Boolean
//啟動 Job
//如果此調用的確啟動了 Job,則返回 true
//如果 Job 調用前就已處于 started 或者是 completed 狀態,則返回 false
public fun start(): Boolean
//用于取消 Job,可同時通過傳入 Exception 來標明取消原因
public fun cancel(cause: CancellationException? = null)
//阻塞等待直到此 Job 結束運行
public suspend fun join()
//當 Job 結束運行時(不管由于什么原因)回調此方法,可用于接收可能存在的運行異常
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
Job 具有以下幾種狀態值,每種狀態對應的屬性值各不相同
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (transient state) | true | false | false |
Cancelling (transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
fun main() {
//將協程設置為延遲啟動
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
for (i in 0..100) {
//每循環一次均延遲一百毫秒
delay(100)
}
}
job.invokeOnCompletion {
log("invokeOnCompletion:$it")
}
log("1. job.isActive:${job.isActive}")
log("1. job.isCancelled:${job.isCancelled}")
log("1. job.isCompleted:${job.isCompleted}")
job.start()
log("2. job.isActive:${job.isActive}")
log("2. job.isCancelled:${job.isCancelled}")
log("2. job.isCompleted:${job.isCompleted}")
//休眠四百毫秒后再主動取消協程
Thread.sleep(400)
job.cancel(CancellationException("test"))
//休眠四百毫秒防止JVM過快停止導致 invokeOnCompletion 來不及回調
Thread.sleep(400)
log("3. job.isActive:${job.isActive}")
log("3. job.isCancelled:${job.isCancelled}")
log("3. job.isCompleted:${job.isCompleted}")
}
[main] 1. job.isActive:false
[main] 1. job.isCancelled:false
[main] 1. job.isCompleted:false
[main] 2. job.isActive:true
[main] 2. job.isCancelled:false
[main] 2. job.isCompleted:false
[DefaultDispatcher-worker-2] invokeOnCompletion:java.util.concurrent.CancellationException: test
[main] 3. job.isActive:false
[main] 3. job.isCancelled:true
[main] 3. job.isCompleted:true
3、async
看下 async
函數的方法簽名。async
也是一個作用于 CoroutineScope 的擴展函數,和 launch
的區別主要就在于:async
可以返回協程的執行結果,而 launch
不行
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
通過await()
方法可以拿到 async 協程的執行結果,可以看到兩個協程的總耗時是遠少于七秒的,總耗時基本等于耗時最長的協程
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async {
delay(3000)
1
}
val asyncB = async {
delay(4000)
2
}
log(asyncA.await() + asyncB.await())
}
}
log(time)
}
[main] 3
[main] 4070
由于 launch 和 async 僅能夠在 CouroutineScope 中使用,所以任何創建的協程都會被該 scope 追蹤。Kotlin 禁止創建不能夠被追蹤的協程,從而避免協程泄漏
4、async 錯誤用法
修改上述代碼,可以發現兩個協程的總耗時就會變為七秒左右
fun main() {
val time = measureTimeMillis {
runBlocking {
val asyncA = async(start = CoroutineStart.LAZY) {
delay(3000)
1
}
val asyncB = async(start = CoroutineStart.LAZY) {
delay(4000)
2
}
log(asyncA.await() + asyncB.await())
}
}
log(time)
}
[main] 3
[main] 7077
會造成這不同區別是因為 CoroutineStart.LAZY
不會主動啟動協程,而是直到調用async.await()
或者async.satrt()
后才會啟動(即懶加載模式),所以asyncA.await() + asyncB.await()
會導致兩個協程其實是在順序執行。而默認值 CoroutineStart.DEFAULT
參數會使得協程在聲明的同時就被啟動了(實際上還需要等待被調度執行,但可以看做是立即就執行了),所以此時調用第一個 async.await()
時兩個協程其實都是處于運行狀態,所以總耗時就是四秒左右
此時可以通過先調用start()
再調用await()
來實現第一個例子的效果
asyncA.start()
asyncB.start()
log(asyncA.await() + asyncB.await())
5、async 并行分解
由 suspend
函數啟動的所有協程都必須在該函數返回結果時停止,因此你可能需要保證這些協程在返回結果之前完成。借助 Kotlin 中的結構化并發機制,你可以定義用于啟動一個或多個協程的 coroutineScope
。然后,你可以使用 await()
(針對單個協程)或 awaitAll()
(針對多個協程)保證這些協程在從函數返回結果之前完成
假設我們定義一個用于異步獲取兩個文檔的 coroutineScope
,通過對每個延遲引用調用 await()
,我們可以保證這兩項 async
操作在返回值之前完成:
suspend fun fetchTwoDocs() = coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
還可以對集合使用 awaitAll()
來達到相同效果。雖然 fetchTwoDocs()
使用 async
啟動新協程,但該函數使用 awaitAll()
等待啟動的協程完成后才會返回結果。不過,即使我們沒有調用 awaitAll()
,coroutineScope
構建器也會等到所有內部協程都完成后才會恢復名為 fetchTwoDocs
的協程。此外,coroutineScope
會捕獲協程拋出的所有異常,并將其傳送給調用方
suspend fun fetchTwoDocs() = coroutineScope {
val deferreds = listOf(
async { fetchDoc(1) },
async { fetchDoc(2) }
)
deferreds.awaitAll()
}
6、Deferred
async
函數的返回值是一個 Deferred 對象。Deferred 是一個接口類型,繼承于 Job 接口,所以 Job 包含的屬性和方法 Deferred 都有,其主要是在 Job 的基礎上擴展了 await()
方法
七、CoroutineContext
CoroutineContext 使用以下元素集定義協程的行為:
- Job:控制協程的生命周期
- CoroutineDispatcher:將任務指派給適當的線程
- CoroutineName:協程的名稱,可用于調試
- CoroutineExceptionHandler:處理未捕獲的異常
1、Job
協程中的 Job 是其上下文 CoroutineContext 中的一部分,可以通過 coroutineContext[Job]
表達式從上下文中獲取到,我們可以通過控制 Job 來控制 CoroutineScope 的生命周期
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
fun main(): Unit = runBlocking {
log("job is $job")
val job = scope.launch {
try {
delay(3000)
} catch (e: CancellationException) {
log("job is cancelled")
throw e
}
log("end")
}
delay(1000)
log("scope job is ${scope.coroutineContext[Job]}")
scope.coroutineContext[Job]?.cancel()
}
[main] job is JobImpl{Active}@759ebb3d
[main] scope job is JobImpl{Active}@759ebb3d
[DefaultDispatcher-worker-1] job is cancelled
實際上 CoroutineScope 的 isActive
這個擴展屬性只是 coroutineContext[Job]?.isActive ?: true
的一種簡便寫法
public val CoroutineScope.isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
2、CoroutineDispatcher
CoroutineContext 包含一個 CoroutineDispatcher(協程調度器)用于指定執行協程的目標載體,即 運行于哪個線程。CoroutineDispatcher 可以將協程的執行操作限制在特定線程上,也可以將其分派到線程池中,或者讓它無限制地運行。所有的協程構造器(如 launch 和 async)都接受一個可選參數,即 CoroutineContext ,該參數可用于顯式指定要創建的協程和其它上下文元素所要使用的 CoroutineDispatcher
要在主線程之外運行代碼,可以指定 Kotlin 協程在 Default 或 IO 調度程序上執行工作。在 Kotlin 中,所有協程都必須在 CoroutineDispatcher 中運行,即使它們在主線程上運行也是如此。協程可以自行暫停,而 CoroutineDispatcher 負責將其恢復
Kotlin 協程庫提供了四個 Dispatcher 用于指定在哪一類線程中執行協程:
- Dispatchers.Default。默認調度器,適合用于執行占用大量 CPU 資源的任務。例如:對列表排序和解析 JSON
- Dispatchers.IO。適合用于執行磁盤或網絡 I/O 的任務。例如:使用 Room 組件、讀寫磁盤文件,執行網絡請求
- Dispatchers.Unconfined。對執行協程的線程不做限制,可以直接在當前調度器所在線程上執行
- Dispatchers.Main。使用此調度程序可用于在 Android 主線程上運行協程,只能用于與界面交互和執行快速工作,例如:更新 UI、調用
LiveData.setValue
fun main() = runBlocking<Unit> {
launch {
log("main runBlocking")
}
launch(Dispatchers.Default) {
log("Default")
launch(Dispatchers.Unconfined) {
log("Unconfined 1")
}
}
launch(Dispatchers.IO) {
log("IO")
launch(Dispatchers.Unconfined) {
log("Unconfined 2")
}
}
launch(newSingleThreadContext("MyOwnThread")) {
log("newSingleThreadContext")
launch(Dispatchers.Unconfined) {
log("Unconfined 4")
}
}
launch(Dispatchers.Unconfined) {
log("Unconfined 3")
}
GlobalScope.launch {
log("GlobalScope")
}
}
[DefaultDispatcher-worker-2] Default
[DefaultDispatcher-worker-1] IO
[DefaultDispatcher-worker-2] Unconfined 1
[DefaultDispatcher-worker-1] Unconfined 2
[MyOwnThread] newSingleThreadContext
[main] Unconfined 3
[MyOwnThread] Unconfined 4
[DefaultDispatcher-worker-1] GlobalScope
[main] main runBlocking
-
launch
在不執行 Dispatchers 的情況下使用時,它從外部的協程作用域繼承上下文和調度器,即和 runBlocking 保持一致,均在 main 線程執行 - IO 和 Default 均依靠后臺線程池來執行
- Unconfined 則不限定具體的線程類型,當前調度器在哪個線程,就在該線程上進行執行,因此上述例子中每個 Unconfined 協程所在線程均不一樣
- GlobalScope 啟動協程時默認使用的調度器是 Dispatchers.Default,因此也是在后臺線程池中執行
-
newSingleThreadContext
用于為協程專門創建一個新的線程,專用線程是一種成本非常昂貴的資源,在實際開發時必須當不再需要時釋放掉線程資源,或者存儲在頂級變量中以便在整個應用程序中進行復用
3、withContext
對于以下代碼,get
方法內使用withContext(Dispatchers.IO)
創建了一個指定在 IO 線程池中運行的代碼塊,該區間內的任何代碼都始終通過 IO 線程來執行。由于 withContext
方法本身就是一個掛起函數,因此 get
方法也必須定義為掛起函數
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
借助協程,你可以細粒度地來調度線程。由于withContext()
支持在不引入回調的情況下控制任何代碼的執行線程池,因此你可以將其應用于非常小的函數,例如從數據庫中讀取數據或執行網絡請求。一種不錯的做法是使用 withContext()
來確保每個函數都是主線程安全的,這意味著,你可以從主線程調用每個函數。這樣,調用方就從不需要考慮應該使用哪個線程來執行函數了
在前面的示例中,fetchDocs()
方法在主線程上執行,不過它可以安全地調用 get
方法,因為get
方法已確保網絡請求會在子線程中執行。由于協程支持 suspend
和 resume
操作,因此 withContext
塊完成后,主線程上的協程會立即根據 get
結果恢復
與基于回調的等效實現相比,withContext()
不會增加額外的開銷。此外,在某些情況下,還可以優化 withContext()
調用,使其超越基于回調的等效實現。例如,如果某個函數需要先后調用十次網絡請求,你可以在最外層調用 withContext()
讓協程只切換一次線程,這樣即使每個網絡請求內部均會使用 withContext()
,它也會留在同一調度程序上,從而避免頻率切換線程。此外,協程還優化了 Dispatchers.Default
與 Dispatchers.IO
之間的切換,以盡可能避免線程切換
使用線程池的調度器(例如
Dispatchers.IO
或Dispatchers.Default
)不能保證代碼塊一直在同一線程上從上到下執行,在某些情況下,協程在suspend
和resume
后可能會將任務移交給另一個線程來執行。這意味著,對于整個withContext()
塊,由于多線程并發之間的原子性和可見性等原因,先后讀取到的線程局部變量可能并非是同個值
4、CoroutineName
CoroutineName 用于為協程指定一個名字,方便調試和定位問題
fun main() = runBlocking<Unit>(CoroutineName("RunBlocking")) {
log("start")
launch(CoroutineName("MainCoroutine")) {
launch(CoroutineName("Coroutine#A")) {
delay(400)
log("launch A")
}
launch(CoroutineName("Coroutine#B")) {
delay(300)
log("launch B")
}
}
}
5、CoroutineExceptionHandler
在下文的異常處理會講到
6、組合上下文元素
有時我們需要為協程上下文定義多個元素,此時就可以用 +
運算符。例如,我們可以同時為協程指定 Dispatcher 和 CoroutineName
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default + CoroutineName("test")) {
log("Hello World")
}
}
而由于 CoroutineContext 是由一組元素組成的,所以加號右側的元素會覆蓋加號左側的元素,從而組成新的 CoroutineContext。比如,(Dispatchers.Main, "name") + (Dispatchers.IO)
的運行結果是:(Dispatchers.IO, "name")
八、取消協程
如果用戶退出啟動了協程的 Activity / Fragment,那正常情況下就應該取消所有協程
job.cancel()
就用于取消協程,job.join()
用于阻塞等待協程運行結束。因為 cancel()
函數調用后會馬上返回而不是等待協程結束后再返回,所以此時協程不一定就是已經停止運行了。如果需要確保協程結束運行后再執行后續代碼,就需要調用 join()
方法來阻塞等待。也可以通過調用 Job 的擴展函數 cancelAndJoin()
來完成相同操作,它結合了 cancel
和 join
兩個操作
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
log("job: I'm sleeping $i ...")
delay(500L)
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancel()
job.join()
log("main: Now I can quit.")
}
[main] job: I'm sleeping 0 ...
[main] job: I'm sleeping 1 ...
[main] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[main] main: Now I can quit.
1、協程可能無法取消
并不是所有協程都可以響應取消操作,協程的取消操作是需要協作 (cooperative) 完成的,協程必須協作才能被取消。協程庫中的所有掛起函數都是可取消的,它們在運行前檢查協程是否被取消了,并在取消時拋出 CancellationException 從而結束整個任務。而如果協程在執行計算任務前沒有判斷自身是否已被取消的話,此時就無法取消協程
所以即使以下代碼主動取消了協程,協程也只會在完成既定循環后才結束運行,因為協程沒有在每次循環前先進行檢查,導致任務不受取消操作的影響
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if (System.currentTimeMillis() >= nextPrintTime) {
log("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancelAndJoin()
log("main: Now I can quit.")
}
[DefaultDispatcher-worker-1] job: I'm sleeping 0 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 1 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[DefaultDispatcher-worker-1] job: I'm sleeping 3 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 4 ...
[main] main: Now I can quit.
為了實現取消協程的目的,就需要為上述代碼加上判斷協程是否還處于可運行狀態的邏輯,當不可運行時就主動退出協程。isActive
是 CoroutineScope 的擴展屬性,就用于判斷協程是否還處于可運行狀態
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) {
if (isActive) {
if (System.currentTimeMillis() >= nextPrintTime) {
log("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
} else {
return@launch
}
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancelAndJoin()
log("main: Now I can quit.")
}
取消協程這個操作類似于在 Java 中調用Thread.interrupt()
方法來向線程發起中斷請求,這兩個操作都不會強制停止協程和線程,外部只是相當于發起一個停止運行的請求,需要依靠協程和線程響應請求后主動停止運行
Java 和 Kotlin 之所以均沒有提供一個可以直接強制停止線程或協程的方法,是因為這個操作可能會帶來各種意想不到的情況。例如,在停止線程或協程的時候,它們可能還持有著某些排他性資源(例如:鎖,數據庫鏈接),如果強制性地停止,它們持有的鎖就會一直無法得到釋放,導致其它線程或協程一直無法得到目標資源,最終就可能導致線程死鎖。所以Thread.stop()
方法目前也是處于廢棄狀態,Java 官方并沒有提供一個可靠的停止線程的方法
2、用 finally 釋放資源
可取消的掛起函數在取消時會拋出 CancellationException,可以依靠try {...} finally {...}
或者 Kotlin 的 use
函數在取消協程后釋放持有的資源
fun main() = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
log("job: I'm sleeping $i ...")
delay(500L)
}
} catch (e: Throwable) {
log(e.message)
} finally {
log("job: I'm running finally")
}
}
delay(1300L)
log("main: I'm tired of waiting!")
job.cancelAndJoin()
log("main: Now I can quit.")
}
[main] job: I'm sleeping 0 ...
[main] job: I'm sleeping 1 ...
[main] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[main] StandaloneCoroutine was cancelled
[main] job: I'm running finally
[main] main: Now I can quit.
3、NonCancellable
如果在上一個例子中的 finally
塊中再調用掛起函數的話,將會導致拋出 CancellationException,因為此時協程已經被取消了。通常我們并不會遇到這種情況,因為常見的資源釋放操作都是非阻塞的,且不涉及任何掛起函數。但在極少數情況下我們需要在取消的協程中再調用掛起函數,此時可以使用 withContext
函數和 NonCancellable
上下文將相應的代碼包裝在 withContext(NonCancellable) {...}
代碼塊中,NonCancellable 就用于創建一個無法取消的協程作用域
fun main() = runBlocking {
log("start")
val launchA = launch {
try {
repeat(5) {
delay(50)
log("launchA-$it")
}
} finally {
delay(50)
log("launchA isCompleted")
}
}
val launchB = launch {
try {
repeat(5) {
delay(50)
log("launchB-$it")
}
} finally {
withContext(NonCancellable) {
delay(50)
log("launchB isCompleted")
}
}
}
//延時一百毫秒,保證兩個協程都已經被啟動了
delay(200)
launchA.cancel()
launchB.cancel()
log("end")
}
[main] start
[main] launchA-0
[main] launchB-0
[main] launchA-1
[main] launchB-1
[main] launchA-2
[main] launchB-2
[main] end
[main] launchB isCompleted
4、父協程和子協程
當一個協程在另外一個協程的協程作用域中啟動時,它將通過 CoroutineScope.coroutineContext
繼承其上下文,新啟動的協程就被稱為子協程,子協程的 Job 將成為父協程 Job 的子 Job。父協程總是會等待其所有子協程都完成后才結束自身,所以父協程不必顯式跟蹤它啟動的所有子協程,也不必使用 Job.join
在末尾等待子協程完成
所以雖然 parentJob 啟動的三個子協程的延時時間各不相同,但它們最終都會打印出日志
fun main() = runBlocking {
val parentJob = launch {
repeat(3) { i ->
launch {
delay((i + 1) * 200L)
log("Coroutine $i is done")
}
}
log("request: I'm done and I don't explicitly join my children that are still active")
}
}
[main] request: I'm done and I don't explicitly join my children that are still active
[main] Coroutine 0 is done
[main] Coroutine 1 is done
[main] Coroutine 2 is done
5、傳播取消操作
一般情況下,協程的取消操作會通過協程的層次結構來進行傳播:如果取消父協程或者父協程拋出異常,那么子協程都會被取消;而如果子協程被取消,則不會影響同級協程和父協程,但如果子協程拋出異常則也會導致同級協程和父協程被取消
對于以下代碼,子協程 jon1 被取消并不影響子協程 jon2 和父協程繼續運行,但父協程被取消后子協程都會被遞歸取消
fun main() = runBlocking {
val request = launch {
val job1 = launch {
repeat(10) {
delay(300)
log("job1: $it")
if (it == 2) {
log("job1 canceled")
cancel()
}
}
}
val job2 = launch {
repeat(10) {
delay(300)
log("job2: $it")
}
}
}
delay(1600)
log("parent job canceled")
request.cancel()
delay(1000)
}
[main] job1: 0
[main] job2: 0
[main] job1: 1
[main] job2: 1
[main] job1: 2
[main] job1 canceled
[main] job2: 2
[main] job2: 3
[main] job2: 4
[main] parent job canceled
6、withTimeout
withTimeout
函數用于指定協程的運行超時時間,如果超時則會拋出 TimeoutCancellationException,從而令協程結束運行
fun main() = runBlocking {
log("start")
val result = withTimeout(300) {
repeat(5) {
delay(100)
}
200
}
log(result)
log("end")
}
[main] start
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
at java.lang.Thread.run(Thread.java:748)
withTimeout
方法拋出的 TimeoutCancellationException 是 CancellationException 的子類,之前我們并未在輸出日志上看到關于 CancellationException 這類異常的堆棧信息,這是因為對于一個已取消的協程來說,CancellationException 被認為是觸發協程結束的正常原因。但對于withTimeout
方法來說,拋出異常是其上報超時情況的一種手段,所以該異常不會被協程內部消化掉
如果不希望因為異常導致協程結束,可以改用withTimeoutOrNull
方法,如果超時就會返回 null
九、異常處理
當一個協程由于異常而運行失敗時,它會傳播這個異常并傳遞給它的父協程。接下來,父協程會進行下面幾步操作:
- 取消它自己的子級
- 取消它自己
- 將異常傳播并傳遞給它的父級
異常會到達層級的根部,且當前 CoroutineScope 所啟動的所有協程都會被取消,但協程并非都是一發現異常就執行以上流程,launch 和 async 在處理異常方面有著一些差異
launch 將異常視為未捕獲異常,類似于 Java 的 Thread.uncaughtExceptionHandler
,當發現異常時就會馬上拋出。async 期望最終通過調用 await 來獲取結果 (或者異常),所以默認情況下它不會拋出異常,這意味著如果使用 async 啟動新的協程,它會靜默地將異常丟棄,直到調用 async.await()
才會得到目標值或者拋出存在的異常
例如,以下的 fetchDocs()
方法由于并沒有調用 Deferred.await()
,因此異常并不會被拋給調用方,而如果使用的是 launch 而非 async 的話,異常就會馬上被拋出
private val ioScope = CoroutineScope(Dispatchers.IO)
private fun fetchDocs() {
ioScope.async {
delay(500)
log("taskA throw AssertionError")
throw AssertionError()
}
}
1、CoroutineExceptionHandler
如果想主動捕獲異常信息,可以使用 CoroutineExceptionHandler 作為協程的上下文元素之一,在這里進行自定義日志記錄或異常處理,它類似于對線程使用 Thread.uncaughtExceptionHandler
。但是,CoroutineExceptionHandler 只會在預計不會由用戶處理的異常上調用,因此在 async 中使用它沒有任何效果,當 async 內部發生了異常且沒有捕獲時,那么調用 async.await()
依然會導致應用崩潰
以下代碼只會捕獲到 launch 拋出的異常
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
log("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException()
}
joinAll(job, deferred)
}
[DefaultDispatcher-worker-2] Caught java.lang.AssertionError
2、SupervisorJob
由于異常導致的取消在協程中是一種雙向關系,會在整個協程層次結構中傳播,那如果我們需要的是單向取消該怎么實現呢?
例如,假設在 Activity 中啟動了多個協程,如果單個協程所代表的子任務失敗了,此時并不一定需要連鎖終止整個 Activity 內部的所有其它協程任務,即此時希望子協程的異常不會傳播給同級協程和父協程。而當 Activity 退出后,父協程的異常(即 CancellationException)又應該連鎖傳播給所有子協程,終止所有子協程
可以使用 SupervisorJob 來實現上述效果,取消操作只會向下傳播,一個子協程的運行失敗不會影響到同級協程和父協程
例如,以下示例中 firstChild 拋出的異常不會導致 secondChild 被取消,但當 supervisor 被取消時 secondChild 也被同時取消了
fun main() = runBlocking {
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
log("First child is failing")
throw AssertionError("First child is cancelled")
}
val secondChild = launch {
firstChild.join()
log("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
log("Second child is cancelled because supervisor is cancelled")
}
}
firstChild.join()
log("Cancelling supervisor")
//取消所有協程
supervisor.cancel()
secondChild.join()
}
}
[main] First child is failing
[main] First child is cancelled: true, but second one is still active
[main] Cancelling supervisor
[main] Second child is cancelled because supervisor is cancelled
但是,如果異常沒有被處理且 CoroutineContext 沒有包含一個 CoroutineExceptionHandler 的話,異常會到達默認線程的 ExceptionHandler。在 JVM 中,異常會被打印在控制臺;而在 Android 中,無論異常在那個 Dispatcher 中發生,都會直接導致應用崩潰。所以如果上述例子中移除了 firstChild 包含的 CoroutineExceptionHandler 的話,就會導致 Android 應用崩潰
十、Android ktx
Android ktx 是包含在 Android Jetpack 及其他 Android 庫中的一組 Kotlin 擴展程序。ktx 擴展程序可以為 Jetpack、Android 平臺及其他 API 提供簡潔的慣用 Kotlin 代碼,這些擴展程序利用了多種 Kotlin 語言功能,其中就包括了對 Kotlin 協程的支持
1、Lifecycle ktx
Lifecycle ktx 為每個 Lifecycle 對象(Activity、Fragment、Process 等)定義了一個 LifecycleScope,該作用域具有生命周期安全的保障,在此范圍內啟動的協程會在 Lifecycle 被銷毀時同時取消,可以使用 lifecycle.coroutineScope
或 lifecycleOwner.lifecycleScope
屬性來拿到該 CoroutineScope
引入依賴
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
}
使用示例
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
//Do Something
}
lifecycle.coroutineScope.launch {
//Do Something
}
}
}
lifecycleScope
和 lifecycle.coroutineScope
兩者是等價的,lifecycleScope
只是 ktx 庫提供的一種簡便寫法。從源碼也可以看到,lifecycleScope 是存儲在抽象類 Lifecycle 的 mInternalScopeRef
字段中,且使用的是 SupervisorJob 和 Dispatchers.Main.immediate
,因此我們不必擔心任意子協程的異常情況會影響到全局的協程任務,且其默認就是在主線程運行協程
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
get() {
while (true) {
val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
if (existing != null) {
return existing
}
val newScope = LifecycleCoroutineScopeImpl(
this,
SupervisorJob() + Dispatchers.Main.immediate
)
if (mInternalScopeRef.compareAndSet(null, newScope)) {
newScope.register()
return newScope
}
}
}
2、ViewModel ktx
ViewModel ktx 庫提供了一個 viewModelScope,用于在 ViewModel 中啟動協程,該作用域的生命周期和 ViewModel 相等,當 ViewModel 回調了 onCleared()
方法時會自動取消該作用域
引入依賴
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
}
例如,以下 fetchDocs()
方法內就依靠 viewModelScope
啟動了一個協程,用于在后臺線程發起網絡請求
class MyViewModel : ViewModel() {
fun fetchDocs() {
viewModelScope.launch {
val result = get("https://developer.android.com")
show(result)
}
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
}
從源碼可以看到其大體實現思路和lifecycleScope
類似,存儲在 ViewModel 類的 mBagOfTags
這個 Map 中,且使用的也是 SupervisorJob 和 Dispatchers.Main.immediate
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
3、LiveData ktx
在某些情況下,我們需要先完成特定的異步計算任務,根據計算結果來向 LiveData 回調值,此時就可以使用 LiveData ktx 提供的 liveData
構建器函數來執行 suspend 函數所代表的異步計算任務(耗時任務),并將結果賦值給 LiveData
引入依賴
dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
}
在以下示例中,loadUser()
是在其它地方聲明的 suspend 函數,你可以使用 liveData
構建器函數異步調用 loadUser()
,然后使用 emit()
來發出結果:
val user: LiveData<User> = liveData {
val data = database.loadUser()
emit(data)
}
從源碼可以看到,我們所傳入的 suspend 任務體 block 最終是會被 CoroutineLiveData 包裝為一個 BlockRunner 對象,而 CoroutineLiveData 會在自身開始有 Observer 監聽時執行 blockRunner,并在所有 Observer 均被移除時自動 Cancel 掉 blockRunner
public fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
internal class CoroutineLiveData<T>(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
block: Block<T>
) : MediatorLiveData<T>() {
private var blockRunner: BlockRunner<T>?
private var emittedSource: EmittedSource? = null
init {
val supervisorJob = SupervisorJob(context[Job])
val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
blockRunner = BlockRunner(
liveData = this,
block = block,
timeoutInMs = timeoutInMs,
scope = scope
) {
blockRunner = null
}
}
override fun onActive() {
super.onActive()
blockRunner?.maybeRun()
}
override fun onInactive() {
super.onInactive()
blockRunner?.cancel()
}
}
internal class BlockRunner<T>(
private val liveData: CoroutineLiveData<T>,
private val block: Block<T>,
private val timeoutInMs: Long,
private val scope: CoroutineScope,
private val onDone: () -> Unit
) {
// currently running block job.
private var runningJob: Job? = null
// cancelation job created in cancel.
private var cancellationJob: Job? = null
@MainThread
fun maybeRun() {
cancellationJob?.cancel()
cancellationJob = null
if (runningJob != null) {
return
}
runningJob = scope.launch {
val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext)
block(liveDataScope)
onDone()
}
}
@MainThread
fun cancel() {
if (cancellationJob != null) {
error("Cancel call cannot happen without a maybeRun")
}
cancellationJob = scope.launch(Dispatchers.Main.immediate) {
delay(timeoutInMs)
if (!liveData.hasActiveObservers()) {
runningJob?.cancel()
runningJob = null
}
}
}
}
十一、參考資料
本文參考了以下文章中的很多資料,在此表示感謝