接《Android開發者快速上手Kotlin(六) 之協程上下文和攔截器》文章繼續。
14 協程官方架構初步
經前面兩篇文章介紹了協程的概念和語言級别的文法後,相信你已經對協程有了一個大概的認識了。今天這篇文章主要是以應用層面來介紹Kotlin官方協程架構——kotlinx.coroutines。如果我們平時日常開發使用的是我們之前介紹的語言級别文法那是非常難用的,甚至你可能還會覺得不如使用以前Java中使用線程+接口回調的方式還比它好,而官方提供的協程架構kotlinx.coroutines它是基于文法實作的特性進行封裝,這樣我們在使用起來就會非常友善,我們的代碼就會真正地發揮協程的優勢。
14.1 架構的引用
Kotlin協程官方架構首頁是:https://github.com/Kotlin/kotlinx.coroutines,我們要在項目代碼中對它進行引用,隻需要在Gradle中進行以下配置即可:
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
}
14.2 架構使用
14.2.1 協程Hello World(GlobalScope.launch)
示例
fun main() {
GlobalScope.launch { // 在應用程式的生命周期内啟動一個新的協程并繼續
delay(1000L) // 非阻塞的等待1秒鐘
println("【${Thread.currentThread().name}】World!")
}
println("【${Thread.currentThread().name}】Hello,")
Thread.sleep(2000L) // 阻塞的等待2秒鐘,因為協程的生命周期受應用程式生命周期限制,是以這裡保證協程内部邏輯執行完
}
運作結果
【main】Hello,
【DefaultDispatcher-worker-1】World!
解說
- GlobalScope.launch用于啟動了一個運作在子線程的頂層協程。GlobalScope繼承于CoroutineScope(協程的生命周期),表示此協程的生命周期随應用程式的生命周期,是以如果上面代碼中不對主線程進行Thread.sleep(2000L)的話,主線程main方法運作結束後,應用程式生命周期就會結果,然而協程中的代碼未能及時得到響應。
- delay 是一個特殊的挂起函數 ,它不會造成線程阻塞,但是會挂起協程,并且隻能在協程中使用。
14.2.2 阻塞線程的協程(runBlocking)
示例1
fun main() {
GlobalScope.launch { // 在應用程式的生命周期内啟動一個新的協程并繼續
delay(1000L) // 非阻塞的等待1秒鐘
println("【${Thread.currentThread().name}】World!")
}
println("【${Thread.currentThread().name}】Hello,")
// Thread.sleep(2000L) // 阻塞的等待2秒鐘,因為協程的生命周期受應用程式生命周期限制,是以這裡保證協程内部邏輯執行完
runBlocking { // 主線程中啟動一個阻塞的協程
delay(2000L) // 使用非阻塞的等待2秒鐘,這裡仍然是阻塞
}
}
示例2
fun main() = runBlocking<Unit> {
GlobalScope.launch {
delay(1000L)
println("【${Thread.currentThread().name}】World!")
}
println("【${Thread.currentThread().name}】Hello,")
delay(2000L)
}
兩個示例運作結果一樣
【main】Hello,
【DefaultDispatcher-worker-1】World!
解說
- runBlocking用于啟動了一個運作在主線程的協程,這是會阻塞主線程的。
- 使有示例2,即runBlocking 來包裝 main 函數的執行其運作效果是一樣的。
- runBlocking 作用于最外層,也就是作用于協程的範圍。可以建立一個阻塞目前線程的協程。然而它主要被用來在main函數中或者測試中使用,沒有多大意義。
14.2.3 等待協程(Job.join)
示例
fun main() = runBlocking<Unit> {
val job = GlobalScope.launch {
delay(1000L)
println("【${Thread.currentThread().name}】World!")
}
println("【${Thread.currentThread().name}】Hello,")
job.join() // 等待直到子協程執行結束
}
運作結果
【main】Hello,
【DefaultDispatcher-worker-1】World!
解說
- 協程通過GlobalScope.launch啟動後可傳回一個Job對象,通過Job對象的join函數可對協程進行等待。
- join函數隻能運作在suspend函數中,或者說隻能在協程内部使用,是以可見上述代碼中是運作在runBlocking啟動的協程中去等待GlobalScope.launch啟動的協程。
14.2.4 取消協程(Job.cancel)
示例1:正常取消
fun main() = runBlocking {
val job = GlobalScope.launch {
repeat(1000) { i -> // 啟動1000個協程
println("【${Thread.currentThread().name}】協程工作中: $i ...")
delay(500L)
}
}
delay(1100L)
println("【${Thread.currentThread().name}】準備取消協程")
job.cancel() // 取消該作業
job.join() // 等待作業執行結束,兩個函數結合可用:job.cancelAndJoin()
println("【${Thread.currentThread().name}】已經取消協程")
}
運作結果1
【DefaultDispatcher-worker-1】協程工作中: 0 ...
【DefaultDispatcher-worker-1】協程工作中: 1 ...
【DefaultDispatcher-worker-1】協程工作中: 2 ...
【main】準備取消協程
【main】已經取消協程
解說1
- 同樣在協程通過GlobalScope.launch啟動後可傳回一個Job對象,通過Job對象的cancel函數可對協程進行取消。
- 示例中通過repeat啟動了1000個協程,然後每個協程裡進行列印和耗時操作。
- 一般cancel和join一起使用,也可以使用cancelAndJoin函數來合并使用。
示例2:計算任務不能取消
fun main() = runBlocking {
// val job = GlobalScope.launch {
// repeat(1000) { i -> // 啟動1000個協程
// println("【${Thread.currentThread().name}】協程工作中: $i ...")
// delay(500L)
// }
// }
val startTime = System.currentTimeMillis()
val job = GlobalScope.launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 10) { // 一個執行計算的循環,隻是為了占用 CPU
if (System.currentTimeMillis() >= nextPrintTime) {
println("協程工作中: ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1100L)
println("【${Thread.currentThread().name}】準備取消協程")
job.cancel() // 取消該作業
job.join() // 等待作業執行結束,兩個函數結合可用:job.cancelAndJoin()
println("【${Thread.currentThread().name}】已經取消協程")
}
運作結果2
【DefaultDispatcher-worker-1】協程工作中: 0 ...
【DefaultDispatcher-worker-1】協程工作中: 1 ...
【DefaultDispatcher-worker-1】協程工作中: 2 ...
【main】準備取消協程
【DefaultDispatcher-worker-1】協程工作中: 3 ...
【DefaultDispatcher-worker-1】協程工作中: 4 ...
【DefaultDispatcher-worker-1】協程工作中: 5 ...
【DefaultDispatcher-worker-1】協程工作中: 6 ...
【DefaultDispatcher-worker-1】協程工作中: 7 ...
【DefaultDispatcher-worker-1】協程工作中: 8 ...
【DefaultDispatcher-worker-1】協程工作中: 9 ...
【main】已經取消協程
解說2
- 協程代碼必須協作才能被取消。 所有kotlinx.coroutines中的挂起函數都是可被取消的。它們檢查協程的取消, 并在取消時抛出 CancellationException。 然而,如果協程正在執行計算任務,并且沒有檢查取消的話,那麼它是不能被取消的。
- 運作結果中并沒有發現CancellationException異常資訊,那是因為CancellationException 被認為是協程執行結束的正常原因。
示例3:取消計算任務
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = GlobalScope.launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 10 && isActive) { // 一個執行計算的循環,隻是為了占用 CPU
if (System.currentTimeMillis() >= nextPrintTime) {
println("【${Thread.currentThread().name}】協程工作中: ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1100L)
println("【${Thread.currentThread().name}】準備取消協程")
job.cancel() // 取消該作業
job.join() // 等待作業執行結束,兩個函數結合可用:job.cancelAndJoin()
println("【${Thread.currentThread().name}】已經取消協程")
}
運作結果3
【DefaultDispatcher-worker-1】協程工作中: 0 ...
【DefaultDispatcher-worker-1】協程工作中: 1 ...
【DefaultDispatcher-worker-1】協程工作中: 2 ...
【main】準備取消協程
【main】已經取消協程
解說3
- 使用isActive可取消計算計算任務循環,使協程達到取消的效果。
- isActive 是一個可以被使用在 CoroutineScope 中的擴充屬性,它其實是協程上下文中的一個屬性:coroutineContext[Job]?.isActive 。
14.2.5 取消協程後釋放資源(finally)
示例
fun main() = runBlocking {
val job = GlobalScope.launch {
try {
repeat(1000) { i -> // 啟動1000個協程
println("【${Thread.currentThread().name}】協程工作中: $i ...")
delay(500L)
}
} finally {
println("【${Thread.currentThread().name}】協程已被取消,正在釋放資源")
delay(1000L)
println("【${Thread.currentThread().name}】協程已被取消,已經釋放完資源")
}
}
delay(1100L)
println("【${Thread.currentThread().name}】準備取消協程")
job.cancel() // 取消該作業
job.join() // 等待作業執行結束,兩個函數結合可用:job.cancelAndJoin()
println("【${Thread.currentThread().name}】已經取消協程")
}
運作結果
【DefaultDispatcher-worker-1】協程工作中: 0 ...
【DefaultDispatcher-worker-1】協程工作中: 1 ...
【DefaultDispatcher-worker-1】協程工作中: 2 ...
【main】準備取消協程
【DefaultDispatcher-worker-1】協程已被取消,正在釋放資源
【main】已經取消協程
解說
- 使用try {……} finally {……} 表達式可在協程取消抛出CancellationException時進行資源的釋放工作。
- 請注意,上述示例中,finally中代碼僅被執行了開頭,delay後面并未被執行到,我們下面解釋。
14.2.6 指定協程上運作代碼塊(withContext)
示例
fun main() = runBlocking {
val job = GlobalScope.launch {
try {
repeat(1000) { i -> // 啟動1000個協程
println("【${Thread.currentThread().name}】協程工作中: $i ...")
delay(500L)
}
} finally {
withContext(NonCancellable) {
println("【${Thread.currentThread().name}】協程已被取消,正在釋放資源")
delay(1000L)
println("【${Thread.currentThread().name}】協程已被取消,已經釋放完資源")
}
}
}
delay(1100L)
println("【${Thread.currentThread().name}】準備取消協程")
job.cancel() // 取消該作業
job.join() // 等待作業執行結束,兩個函數結合可用:job.cancelAndJoin()
println("【${Thread.currentThread().name}】已經取消協程")
}
運作結果
【DefaultDispatcher-worker-1】協程工作中: 0 ...
【DefaultDispatcher-worker-1】協程工作中: 1 ...
【DefaultDispatcher-worker-1】協程工作中: 2 ...
【main】準備取消協程
【DefaultDispatcher-worker-1】協程已被取消,正在釋放資源
【DefaultDispatcher-worker-1】協程已被取消,已經釋放完資源
【main】已經取消協程
解說
- 當需要挂起一個被取消的協程,可以将相應的代碼包裝在 withContext(NonCancellable) {……} 中,并使用 withContext 函數以及 NonCancellable 上下文,這裡持續運作的代碼就不會被取消。
14.2.7 協程的逾時(withTimeout / withTimeoutOrNull)
示例1:withTimeout
fun main() = runBlocking {
try {
withTimeout(1100L) {
repeat(1000) { i ->
println("【${Thread.currentThread().name}】協程工作中: $i ...")
delay(500L)
}
}
} catch (e: TimeoutCancellationException) {
println("【${Thread.currentThread().name}】協程逾時了,${e.message}")
}
}
運作結果1
【main】協程工作中: 0 ...
【main】協程工作中: 1 ...
【main】協程工作中: 2 ...
【main】協程逾時了,Timed out waiting for 1100 ms
解說1
1. withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子類。
示例2:withTimeoutOrNull
fun main() = runBlocking {
val result = withTimeoutOrNull(1100L) {
repeat(1000) { i ->
println("【${Thread.currentThread().name}】協程工作中: $i ...")
delay(500L)
}
"Done" // 正常結束傳回的結果
}
println("【${Thread.currentThread().name}】協程運作的結果是: $result")
}
運作結果2
【main】協程工作中: 0 ...
【main】協程工作中: 1 ...
【main】協程工作中: 2 ...
【main】協程運作的結果是: null
解說2
- 通過withTimeoutOrNull處理協程逾時時不會抛出異常,而是傳回一個null結果
14.2.8 組合挂起函數(async/await并發使用)
假設我們存在兩個耗時的進行計算的挂起函數。現在我們需要對它們進行計算的結果進行總彙處理。
示例1
fun main() = runBlocking {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("【${Thread.currentThread().name}】計算結果: ${one + two}")
}
println("【${Thread.currentThread().name}】共耗時: $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L)
println("【${Thread.currentThread().name}】doSomethingUsefulOne 計算中")
return 2
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L)
println("【${Thread.currentThread().name}】doSomethingUsefulTwo 計算中")
return 3
}
運作結果1
【main】doSomethingUsefulOne 計算中
【main】doSomethingUsefulTwo 計算中
【main】計算結果: 5
【main】共耗時: 2012 ms
解說1
1.上面示例代碼中,doSomethingUsefulOne函數執行完畢後才到doSomethingUsefulTwo函數,它們是順度執行的,是以它們的總耗時是兩個函數執行時間之和。
示例2
fun main() = runBlocking {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("【${Thread.currentThread().name}】計算結果: ${one.await() + two.await()}")
}
println("【${Thread.currentThread().name}】共耗時: $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L)
println("【${Thread.currentThread().name}】doSomethingUsefulOne 計算中")
return 2
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L)
println("【${Thread.currentThread().name}】doSomethingUsefulTwo 計算中")
return 3
}
運作結果2
【main】doSomethingUsefulOne 計算中
【main】doSomethingUsefulTwo 計算中
【main】計算結果: 5
【main】共耗時: 1020 ms
解說2
- 如果要計算的兩個函數不需要依賴關系時,可使用 async進行執行并發來節省運作時間。
- async 類似于 launch。它啟動了一個單獨的協程,這是一個輕量級的線程并與其它所有的協程一起并發的工作。不同之處在于 launch 傳回一個 Job 并且不附帶任何結果值,而 async 傳回一個Deferred(一個輕量級的非阻塞 future), 這代表了一個将會在稍後提供結果的promise。你可以使用 .await() 在一個延期的值上得到它的最終結果。
- Deferred 也是一個 Job,是以如果需要的話,也可以通過cancel對它進行取消。
14.2.9 協程上下文
我們在上一篇文章《Android開發者快速上手Kotlin(六) 之 協程上下文和攔截器》針對Kotlin語言層面的協程學習中有介紹過,協程上下文就是一個在執行過程中攜帶資料的載體對象,它其實就是一個用Key作索引,Element作元素的集合,一般用于資料從協程外層傳遞協程内部。我們在使用協程架構同樣也是可以進行上下文的自定義的。
示例
class ParameterContext(val isSuccess: Boolean) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<ParameterContext>
}
fun main() = runBlocking {
launch(CoroutineName("HelloWorld") + ParameterContext(true)) {
println("Job的上下文對象是:${coroutineContext[Job]}")
println("傳遞的參數值是:${coroutineContext[ParameterContext]?.isSuccess}")
println("協程的名字是:${coroutineContext[CoroutineName]?.name}")
}
println("Main函數執行完畢")
}
運作結果
Main函數執行完畢
Job的上下文對象是:StandaloneCoroutine{Active}@369f73a2
傳遞的參數值是:true
協程的名字是:HelloWorld
解說
- launch函數實質上是接收3個參數,其中第一個參數是CoroutineContext類型,也就是協程上下文。
- 通過繼承AbstractCoroutineContextElement接口來自定義上下文。
- CoroutineName是架構提供的上下文類,用于定義協程的名稱。
- 多個上下文可以使用“+”來進行add。
- 協程的 Job 是上下文的一部分,并且可以使用 coroutineContext [Job] 表達式在上下文中檢索它。
- 前面介紹取消協程時,取消計算任務中使用到的 isActive 其實本質上就是 coroutineContext[Job]?.isActive。
14.2.10 協程的生命周期
我們在協程Hello World中提到,GlobalScope.launch用于啟動了一個運作在子線程的頂層協程,這個頂層的意思就是它的生命周期随應用程式的生命周期。雖然協程是輕量級的,但是它的運作仍然會消耗一些記憶體資源。是以我們完全可以在指定作用域内去啟動協程。
示例
fun main() = runBlocking { // this: CoroutineScope
launch {
delay(200L)
println("【${Thread.currentThread().name}】在 runBlocking 裡的任務")
}
coroutineScope { // 建立一個協程作用域
launch {
delay(500L)
println("【${Thread.currentThread().name}】在 coroutineScope 裡嵌套 launch 中的任務")
}
delay(100L)
println("【${Thread.currentThread().name}】在 coroutineScope 裡的任務")
}
println("【${Thread.currentThread().name}】在 Main 裡的任務")
}
運作結果
【main】在 coroutineScope 裡的任務
【main】在 runBlocking 裡的任務
【main】在 coroutineScope 裡嵌套 launch 中的任務
【main】在 Main 裡的任務
解說
- 因為runBlocking 協程建構器将 main 函數轉換為協程,包括 runBlocking 在内的每個協程建構器都将 CoroutineScope 的執行個體添加到其代碼塊所在的作用域中,是以省略GlobalScope,直接使用launch就是等于在此作用域中啟動協程。
- 使用 coroutineScope 建構器聲明自己的作用域。它會建立一個協程作用域并且在所有裡機已啟動的協程執行完畢之前不會結束。
- runBlocking 與 coroutineScope主要差別在于,runBlocking 方法會阻塞目前線程來等待, 而 coroutineScope 隻是挂起,會釋放底層線程用于其他用途。 由于存在這點差異,runBlocking 是正常函數,而 coroutineScope 是挂起函數。
14.2.11 協程的嵌套關系
示例1
fun main() = runBlocking {
val job = launch {
GlobalScope.launch {
println("嵌套的頂層協程開始工作")
delay(1000)
println("嵌套的頂層協程結束工作")
}
launch {
println("嵌套的子協程開始工作")
delay(1000)
println("嵌套的子協程結束工作")
}
}
delay(500)
job.cancel()
delay(2000)
println("Main函數執行完畢")
}
運作結果1
嵌套的頂層協程開始工作
嵌套的子協程開始工作
嵌套的頂層協程結束工作
Main函數執行完畢
解說1
- 當一個協程被一個協程嵌套啟動時,它們就是父子關系,子協程會繼承父協程的 CoroutineScope.coroutineContext,并且子協程Job也會成為父協程Job的子Job,當父協程被取消時,是以它的子協程也會被遞歸取消。
- 當我們使用GlobalScope來啟動一個頂層協程時,被嵌套的頂層協程是沒有父Job,是以它與這個“父協程”沒有運作關系。
示例2
fun main() = runBlocking {
val job = launch {
repeat(3) { i -> // 啟動3個子協程
launch {
delay((i + 1) * 200L)
println("子協程 $i 執行完畢")
}
}
println("父協程已經執行完畢")
}
job.join()
println("Main函數執行完畢")
}
運作結果2
父協程已經執行完畢
子協程 0 執行完畢
子協程 1 執行完畢
子協程 2 執行完畢
Main函數執行完畢
解說2
- 父協程一定會等待所有的子協程執行結束後它的生命周期才結束。
- 父協程并不顯式的跟蹤所有子協程的啟動,是以不需要專門使用 Job.join 等待子協程。
14.2.12 協程排程器
我們在上一篇文章《Android開發者快速上手Kotlin(六) 之 協程上下文和攔截器》針對Kotlin語言層面的協程學習中還介紹過攔截器,攔截器是對協程上下文所在的協程進行攔截,進而達到線程切換效果。然而所謂協程排程器本質就是上下文+攔截器的封裝,我們在使用架構來啟動一個協程時,也是可以通過傳入指定的協程上下文來将協程限制在一個特定的線程執行。
示例
fun main() = runBlocking {
launch { // 不傳參數就是 EmptyCoroutineContext
println("【${Thread.currentThread().name}】EmptyCoroutineContext 運作在父協程的所線上程的協程")
}
launch(Dispatchers.Unconfined) {
println("【${Thread.currentThread().name}】Unconfined 運作在目前線程的協程1")
delay(1000L)
println("【${Thread.currentThread().name}】Unconfined 運作在目前線程的協程2")
}
// launch(Dispatchers.Main) { // 該代碼需要運作在Android工程中,并在Gradle中依賴org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version
// println("【${Thread.currentThread().name}】運作在UI線程的協程")
// }
launch(Dispatchers.Default) { // GlobalScope.launch 就是使用了 Dispatchers.Default
println("【${Thread.currentThread().name}】Default 運作線上程池的協程,用于處理CPU密集型工作")
}
launch(Dispatchers.IO) {
println("【${Thread.currentThread().name}】IO 運作線上程池的協程,用于處理IO密集型工作")
}
launch(newSingleThreadContext("Myhread")) {
println("【${Thread.currentThread().name}】運作在指定線程的協程")
}
println("【${Thread.currentThread().name}】Main函數執行完畢")
}
運作結果
【main】Unconfined 運作在目前線程的協程1
【DefaultDispatcher-worker-1】Default 運作線上程池的協程,用于處理CPU密集型工作
【DefaultDispatcher-worker-2】IO 運作線上程池的協程,用于處理IO密集型工作
【main】Main函數執行完畢
【main】EmptyCoroutineContext 運作在父協程的所線上程的協程
【Myhread】運作在指定線程的協程
【kotlinx.coroutines.DefaultExecutor】Unconfined 運作在目前線程的協程2
解說
- Default和IO它們的背後都是一個線程池,差別在于IO使用到的線程池裡線程隊列是無限的,使用上Default适用于CPU密集型,也就是一些運算類型的操作;而IO适用于IO密集型,例如網絡請求、檔案讀寫等。
- 請注意Unconfined的運作結果,它的兩次列印是在不同的線程中完成的。因為使用Unconfined啟動協程首先會運作在目前線程上,但是隻是在第一個挂起點之前是這樣的,挂起恢複後運作在哪個線程完全由所調用的挂起函數決定。
- Unconfined屬于非受限的排程器,它非常适用于執行不消耗 CPU 時間的任務,以及不更新局限于特定線程的任何共享資料(如UI)的協程。其實一般也不怎麼使用。
14.2.13 協程的啟動模式
協程的啟動模式一共有4種, 我們從其源碼可見它是一個枚舉類,它們差別在其注釋中已經說明得很清楚,這裡我将其翻譯成中文并用一句話進行了概括:
public enum class CoroutineStart {
// 立即開始執行協程體,随時可以取消
DEFAULT,
// 隻有在需要(start/join/await)時開始執行
LAZY,
// 立即開始執行協程體,且在第一個挂起點前不能被取消
@ExperimentalCoroutinesApi
ATOMIC,
// 立即在目前線程執行協程體,直到遇到第一個挂起點
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
示例1
fun main() = runBlocking {
launch(start = CoroutineStart.DEFAULT) {
println("【${Thread.currentThread().name}】協程開始")
delay(100)
println("【${Thread.currentThread().name}】協程結束")
}
println("【${Thread.currentThread().name}】Main函數執行完畢")
}
運作結果1
【main】Main函數執行完畢
【main】協程開始
【main】協程結束
示例2
fun main() = runBlocking {
launch(start = CoroutineStart.UNDISPATCHED) {
println("【${Thread.currentThread().name}】協程開始")
delay(100)
println("【${Thread.currentThread().name}】協程結束")
}
println("【${Thread.currentThread().name}】Main函數執行完畢")
}
運作結果2
【main】協程開始
【main】Main函數執行完畢
【main】協程結束
解說
- 我們在啟動協程通過使用lauch函數,通過指定它的第二個參數start來決定協程的啟動模式。
- LAZY模式很好了解,就是我們在上一篇文章中介紹createCoroutine和startCoroutine的情況,createCoroutine是建立了協程但未執行,而startCoroutine是建立時就執行。
- DEFAULT模式和ATOMIC模式就是取消時機上的差別,一般我們常用DEFAULT模式,ATOMIC隻有在涉及到cancel的時候才有意義。
- UNDISPATCHED模式就是協程體内代碼會在目前線程先執行,直到遇到挂起點才跳出,如運作結果2。
未完,請關注後面文章更新…