kotlin學(xué)習(xí)總結(jié)之九 協(xié)程(一)

一. 什么是協(xié)程

協(xié)程本質(zhì)是一套由 Kotlin 官方提供的線程 API,可以理解為一個(gè)線程框架。它最大的好處是:可以在同一個(gè)代碼塊中進(jìn)行多次線程切換,簡(jiǎn)化異步任務(wù)處理的方案。

協(xié)程和線程的區(qū)別:

  • 協(xié)程是運(yùn)行在單線程中的并發(fā)程序,避免了多線程并發(fā)機(jī)制中切換線程時(shí)帶來的線程上下文切換、線程狀態(tài)切換、線程初始化上的性能損耗,能大幅度提高并發(fā)性能。

  • 線程是由系統(tǒng)調(diào)度的,線程切換或線程阻塞的開銷都比較大。而協(xié)程依賴于線程,但是協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無代價(jià)的,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程,非常輕量級(jí),一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程。

  • 協(xié)程是跑在線程上的,一個(gè)線程可以同時(shí)跑多個(gè)協(xié)程,每一個(gè)協(xié)程則代表一個(gè)耗時(shí)任務(wù),需要手動(dòng)控制多個(gè)協(xié)程之間的運(yùn)行、切換,決定誰什么時(shí)候掛起,什么時(shí)候運(yùn)行,什么時(shí)候喚醒,而不是線程那樣交給系統(tǒng)內(nèi)核來操作去競(jìng)爭(zhēng)CPU時(shí)間片,缺點(diǎn)是本質(zhì)是個(gè)單線程,不能利用到單個(gè)CPU的多個(gè)核心。

二. 工程配置(引入依賴)

首先,要在工程里引入?yún)f(xié)程。

這里注意,kotlin相關(guān)庫(kù)的版本最好都用1.3.+的版本,并且要符合gradle插件版本在3.0.0版本以上才可以使用。

//project.gradle
classpath 'com.android.tools.build:gradle:3.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.40"

//app.gradle
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.5"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"

三. 協(xié)程作用域(協(xié)程運(yùn)行環(huán)境)

協(xié)程是一套管理和運(yùn)行異步任務(wù)的框架,所以需要有運(yùn)行的環(huán)境,也叫協(xié)程的作用域,在這個(gè)作用域里,才可以使用協(xié)程來執(zhí)行異步任務(wù),協(xié)程作用域是協(xié)程運(yùn)行的作用范圍,換句話說,如果這個(gè)作用域銷毀了,那么里面的協(xié)程也隨之失效。就好比變量的作用域。

1. CoroutineScope 接口

CoroutineScope 是一個(gè)接口,要是查看這個(gè)接口的源代碼的話就發(fā)現(xiàn)這個(gè)接口里面只定義了一個(gè)屬性 CoroutineContext:

public interface CoroutineScope {
    // Scope 的 Context
    public val coroutineContext: CoroutineContext
}

2. GlobalScope(全局作用域)

GlobalScope 是 CoroutineScope 的一個(gè)單例實(shí)現(xiàn),是一個(gè)單例對(duì)象,是默認(rèn)的全局作用域。GlobalScope 實(shí)現(xiàn)了 CoroutineScope 接口,這個(gè)接口持有了協(xié)程上下文。

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

用法:

GlobalScope.launch {
    //...
}

GlobalScope代表協(xié)程的全局作用域,在該作用域啟動(dòng)的協(xié)程為頂層協(xié)程,沒有父任務(wù),且該scope沒有Job對(duì)象(管理任務(wù)),所以無法對(duì)整個(gè)scope執(zhí)行cancel()操作,所以如果沒有手動(dòng)取消每個(gè)任務(wù),會(huì)造成這些任務(wù)一直運(yùn)行(覆水難收),可能會(huì)導(dǎo)致內(nèi)存泄露等問題,所以仍適用于業(yè)務(wù)開發(fā)。

3. 自定義作用域

協(xié)程作用域的創(chuàng)建方式有很多,常見的有:
① 繼承 CoroutineScope 接口自己實(shí)現(xiàn);
② 使用 coroutineScope 方法或者 supervisorScope 方法創(chuàng)建;

① CoroutineScope 接口
在應(yīng)用中具有生命周期的組件應(yīng)該實(shí)現(xiàn) CoroutineScope 接口,并負(fù)責(zé)該組件內(nèi) Coroutine 的創(chuàng)建和管理。

CoroutineScope(Dispatchers.Main).launch {
    //...
}

通常我們會(huì)通過創(chuàng)建CoroutineScope,來實(shí)現(xiàn)一個(gè)自己的協(xié)程作用域,并且可以指定派發(fā)器,在我們需要取消該scope下所有任務(wù)時(shí)(比如Activity退出時(shí)),調(diào)用scope.cancel()方法,就可以取消該scope下所有正在進(jìn)行的任務(wù),這才是我們所期望的。

例如對(duì)于 Android 應(yīng)用來說,可以在 Activity 中實(shí)現(xiàn) CoroutineScope 接口, 例如:

class ScopedActivity : Activity(), CoroutineScope {
    lateinit var job: Job
    // CoroutineScope 的實(shí)現(xiàn)
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()

    /*
     * 注意 coroutine builder 的 scope, 如果 activity 被銷毀了或者該函數(shù)內(nèi)創(chuàng)建的 Coroutine
     * 拋出異常了,則所有子 Coroutines 都會(huì)被自動(dòng)取消。不需要手工去取消。
     */
      launch { // <- 自動(dòng)繼承當(dāng)前 activity 的 scope context,所以在 UI 線程執(zhí)行
          val ioData = async(Dispatchers.IO) { // <- launch scope 的擴(kuò)展函數(shù),指定了 IO dispatcher,所以在 IO 線程運(yùn)行
            // 在這里執(zhí)行阻塞的 I/O 耗時(shí)操作
          }
        // 和上面的并非 I/O 同時(shí)執(zhí)行的其他操作
          val data = ioData.await() // 等待阻塞 I/O 操作的返回結(jié)果
          draw(data) // 在 UI 線程顯示執(zhí)行的結(jié)果
      }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 當(dāng) Activity 銷毀的時(shí)候取消該 Scope 管理的 job。
        // 這樣在該 Scope 內(nèi)創(chuàng)建的子 Coroutine 都會(huì)被自動(dòng)的取消。
        job.cancel()
    }


}

由于所有的 Coroutine 都需要一個(gè) CoroutineScope,所以為了方便創(chuàng)建 Coroutine,在 CoroutineScope 上有很多擴(kuò)展函數(shù),比如 launch、async、actor、cancel 等。

② 使用 coroutineScope 和 supervisorScope 方法創(chuàng)建協(xié)程作用域
coroutineScope 方法可以用來創(chuàng)建一個(gè)子作用域,它只能在另一個(gè)已有的協(xié)程作用域中調(diào)用,例如在另外一個(gè) suspend 方法中調(diào)用。supervisorScope 方法和 coroutineScope 類似,也用于創(chuàng)建一個(gè)子作用域,區(qū)別是 supervisorScope 出現(xiàn)異常時(shí)不影響其他子協(xié)程, coroutineScope 出現(xiàn)異常時(shí)會(huì)把異常拋出。

4. MainScope

在 Android 中會(huì)經(jīng)常需要實(shí)現(xiàn)這個(gè) CoroutineScope,所以為了方便開發(fā)者使用, 標(biāo)準(zhǔn)庫(kù)中定義了一個(gè) MainScope() 函數(shù),該函數(shù)定義了一個(gè)使用 SupervisorJob 和 Dispatchers.Main 為 Scope context 的實(shí)現(xiàn)。

class MainActivity: AppCompatActivity(), CoroutineScope by MainScope() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 在IO線程中,請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)
        launch(Dispatchers.IO) {
            val res = requestService()

            // 在主線程中,更新 UI
            launch {
                updateUi(res)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        // 在 Activity 銷毀時(shí)取消
        cancel()
    }
}

四. 啟動(dòng)協(xié)程

現(xiàn)在有了協(xié)程運(yùn)行環(huán)境和任務(wù)執(zhí)行環(huán)境,接下來要做的就是啟動(dòng)一個(gè)協(xié)程了!
需要構(gòu)造器來啟動(dòng)協(xié)程。官方目前提供的基礎(chǔ)構(gòu)造器有兩個(gè):

  • runBlocking
  • 通過scope對(duì)象,使用launch和async方法創(chuàng)建協(xié)程。

這兩個(gè)構(gòu)造器都會(huì)啟動(dòng)一個(gè)協(xié)程,區(qū)別在于后者不會(huì)阻塞當(dāng)前線程,并且會(huì)返回一個(gè)協(xié)程的引用,而前者會(huì)等待協(xié)程的代碼執(zhí)行結(jié)束,再執(zhí)行剩下的代碼。

1. runBlocking

runBlocking是啟動(dòng)新協(xié)程的一種方法。
runBlocking啟動(dòng)一個(gè)新的協(xié)程,并阻塞它的調(diào)用線程,直到里面的代碼執(zhí)行完畢。

 public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend           
  CoroutineScope.() -> T): T {
        ...
    }

舉例

println("aaaaaaaaa ${Thread.currentThread().name}")

runBlocking {
    for (i in 0..10) {
        println("$i ${Thread.currentThread().name}")
        delay(100)
    }
}

println("bbbbbbbbb ${Thread.currentThread().name}")

上面代碼的輸出為:

aaaaaaaaa main
0 main
1 main
2 main
3 main
4 main
5 main
6 main
7 main
8 main
9 main
10 main
bbbbbbbbb main

所有的代碼都在主線程執(zhí)行,按照順序來,去掉runBlocking也是一樣的。

但是,runBlocking可以指定參數(shù),就可以讓runBlocking里面的代碼在其他線程執(zhí)行,但同樣可以阻塞外部線程。

println("aaaaaaaaa ${Thread.currentThread().name}")
runBlocking(Dispatchers.IO) { // 注意這里
    for (i in 0..10) {
        println("$i ${Thread.currentThread().name}")
        delay(100)
    }
}
println("bbbbbbbbb ${Thread.currentThread().name}")

上面的代碼,給runBlocking添加了一個(gè)參數(shù),Dispatchers.IO,這樣里面的代碼塊就會(huì)執(zhí)行到其他線程了。

來一起看看效果:

aaaaaaaaa main
0 DefaultDispatcher-worker-1
1 DefaultDispatcher-worker-1
2 DefaultDispatcher-worker-1
3 DefaultDispatcher-worker-4
4 DefaultDispatcher-worker-4
5 DefaultDispatcher-worker-6
6 DefaultDispatcher-worker-7
7 DefaultDispatcher-worker-7
8 DefaultDispatcher-worker-9
9 DefaultDispatcher-worker-1
10 DefaultDispatcher-worker-5
bbbbbbbbb main

2. launch方法

launch 是最常見的協(xié)程構(gòu)建器,它會(huì)啟動(dòng)一個(gè)新的協(xié)程(AbstractCoroutine),并將這個(gè)協(xié)程對(duì)象返回,接著會(huì)在協(xié)程中執(zhí)行參數(shù)中的 block。不會(huì)阻塞調(diào)用線程。

AbstractCoroutine 繼承了 Job,launch 返回的 Job 對(duì)象實(shí)際就是協(xié)程對(duì)象本身。

launch 的原型如下:

public fun CoroutineScope.launch(
    /** 上下文 */
    context: CoroutineContext = EmptyCoroutineContext,
    
    /** 如何啟動(dòng) */
    start: CoroutineStart = CoroutineStart.DEFAULT,
    
    /** 啟動(dòng)后要執(zhí)行的代碼 */
    block: suspend CoroutineScope.() -> Unit
): Job

launch 方法有兩個(gè)可選參數(shù):CoroutineContext 和 CoroutineStart。

  • CoroutineContext:
    是協(xié)程的上下文,默認(rèn)使用 EmptyCoroutineContext,最主要的兩個(gè)元素是:Job、Dispatcher。
    Job控制協(xié)程的開始、取消等,Dispatchers作用是決定把協(xié)程派發(fā)到哪個(gè)線程中執(zhí)行,與指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默認(rèn)運(yùn)行在一個(gè)后臺(tái)工作線程內(nèi)。也可以通過顯示指定參數(shù)來更改協(xié)程運(yùn)行的線程,Dispatchers提供了幾個(gè)值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。

  • CoroutineStart:
    協(xié)程的啟動(dòng)模式。默認(rèn)的(也是最常用的)CoroutineStart.DEFAULT是指協(xié)程立即執(zhí)行,除此之外還有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。

  • block:
    協(xié)程主體。也就是要在協(xié)程內(nèi)部運(yùn)行的代碼,block是一個(gè)suspend匿名方法,可以通過lamda表達(dá)式的方式方便的編寫協(xié)程內(nèi)運(yùn)行的代碼。

  • 返回值Job:
    Job是launch方法的返回值,它就是用來控制協(xié)程的運(yùn)行狀態(tài)的。Job中有幾個(gè)關(guān)鍵方法:

    • start。如果是CoroutineStart.LAZY創(chuàng)建出來的協(xié)程,調(diào)用該方法開啟協(xié)程。
    • cancel。取消正在執(zhí)行的協(xié)程。如協(xié)程處于運(yùn)算狀態(tài),則不能被取消。也就是說,只有協(xié)程處于阻塞狀態(tài)時(shí)才能夠取消。
    • join。阻塞父協(xié)程,直到本協(xié)程執(zhí)行完。
    • cancelAndJoin。等價(jià)于cancel + join。

3. async方法

async 比較常見,它也會(huì)啟動(dòng)新的協(xié)程(AbstractCoroutine),并返回這個(gè)協(xié)程對(duì)象,然后在協(xié)程中執(zhí)行 block。它與launch類似,差別在于返回值。async方法返回一個(gè)Deferred<T>類型。
Deferred繼承自Job,最主要的是增加了await方法,通過await方法返回T。Deferred.await在等待返回值時(shí)會(huì)阻塞當(dāng)前的協(xié)程。

參數(shù)和 launch 一樣,我們看看 async 怎么獲取返回值:

// 任務(wù)1:耗時(shí)一秒后返回100
val coroutine1 = GlobalScope.async {
    delay(1000)
    return@async 100
}

// 任務(wù)2:耗時(shí)1秒后返回200
val coroutine2 = GlobalScope.async {
    delay(1000)
    return@async 200
}

// 上面兩個(gè)協(xié)程會(huì)并發(fā)執(zhí)行

// 等待兩個(gè)任務(wù)都執(zhí)行完畢后,再繼續(xù)下一步(打印結(jié)果)。
GlobalScope.launch {
    val v1 = coroutine1.await()
    val v2 = coroutine2.await()
    log("執(zhí)行的結(jié)果,v1 = $v1, v2=$v2")
}

4. launch和async方法創(chuàng)建協(xié)程區(qū)別。

CoroutineScope(Dispatchers.IO).launch {  }
CoroutineScope(Dispatchers.IO).async {  }

而兩者的最大不同是,async會(huì)創(chuàng)建一個(gè)Deferred的協(xié)程,可以用來等待該協(xié)程執(zhí)行完畢再進(jìn)行后續(xù)操作。
runBlocking { }在當(dāng)前線程啟動(dòng)一個(gè)協(xié)程,阻塞當(dāng)前線程。也是一個(gè)協(xié)程,不過一般不這樣使用

CoroutineScope(Dispatchers.IO).launch {
  val job = async {  }
  val data = job.await()
  //do something with data
}

上述代碼,在launch的協(xié)程執(zhí)行到await()方法時(shí),會(huì)將協(xié)程掛起(而不是線程掛起,不會(huì)阻塞線程),等待async異步任務(wù)執(zhí)行完成后,會(huì)返回結(jié)果到data,從而進(jìn)行后續(xù)邏輯。

當(dāng)然,如果在調(diào)用await()方法時(shí),async協(xié)程已經(jīng)執(zhí)行完畢擁有了結(jié)果,那么不會(huì)掛起協(xié)程,而是直接返回結(jié)果到data變量里。

五. 協(xié)程派發(fā)器(任務(wù)執(zhí)行環(huán)境)

有了運(yùn)行環(huán)境執(zhí)行異步任務(wù),還需要有派發(fā)器將不同的任務(wù)派發(fā)到不同的線程執(zhí)行,在lanch()函數(shù)中有一個(gè)CoroutineContext 參數(shù),該參數(shù)就是制定任務(wù)環(huán)境的參數(shù)。這也是我們經(jīng)常遇到的,比如網(wǎng)絡(luò)請(qǐng)求在工作線程,結(jié)果回來后的UI展示,需要在主線程進(jìn)行。協(xié)程調(diào)度器可以將協(xié)程的執(zhí)行局限在指定的線程中,調(diào)度它運(yùn)行在線程池中或讓它不受限的運(yùn)行。kotlin給我們提供了一些默認(rèn)的Dispatcher:

  • Dispatchers.IO:工作線程池,依賴于Dispatchers.Default,支持最大并行任務(wù)數(shù)。這個(gè)調(diào)度器被優(yōu)化在主線程之外執(zhí)行磁盤或網(wǎng)絡(luò) I/O。例如包括使用 Room 組件、讀寫文件,以及任何網(wǎng)絡(luò)操作。

  • Dispatchers.Main:主線程,這個(gè)在不同平臺(tái)定義不一樣,所以需要引入相關(guān)的依賴,比如Android平臺(tái),需要使用包含MainLooper的handler來向主線程派發(fā)。使用這個(gè)調(diào)度器在 Android 主線程上運(yùn)行一個(gè)協(xié)程。這應(yīng)該只用于與 UI 交互和一些快速工作。示例包括調(diào)用掛起函數(shù)、運(yùn)行 Android UI 框架操作和更新 LiveData 對(duì)象。

  • Dispatchers.Default:默認(rèn)線程池,核心線程和最大線程數(shù)依賴cpu數(shù)量。這個(gè)調(diào)度器經(jīng)過優(yōu)化,可以在主線程之外執(zhí)行 cpu 密集型的工作。例如對(duì)列表進(jìn)行排序和解析 JSON。

  • Dispatchers.Unconfined:無指定派發(fā)線程,會(huì)根據(jù)運(yùn)行時(shí)的上線文環(huán)境決定。

通常我們用的就是Dispatchers.IO和 Dispatchers.Main,在創(chuàng)建scope時(shí),可以指定派發(fā)器,如CoroutineScope(Dispatchers.Main),就是指定該scope啟動(dòng)的協(xié)程,都在主線程執(zhí)行。調(diào)度器實(shí)現(xiàn)了CoroutineContext接口。

六. 啟動(dòng)模式

在lanch()函數(shù)中有一個(gè)CoroutineContext 參數(shù),該參數(shù)就是制定任務(wù)環(huán)境的參數(shù)。在Kotlin協(xié)程當(dāng)中,啟動(dòng)模式定義在一個(gè)枚舉類中:

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;
}

一共定義了4種啟動(dòng)模式,下表是含義介紹:

啟動(dòng)模式 作用
DEFAULT 默認(rèn)的模式,立即執(zhí)行協(xié)程體
LAZY 只有在需要的情況下運(yùn)行
ATOMIC 立即執(zhí)行協(xié)程體,但在開始運(yùn)行之前無法取消
UNDISPATCHED 立即在當(dāng)前線程執(zhí)行協(xié)程體,直到第一個(gè) suspend 調(diào)用

七. 掛起函數(shù)

協(xié)程體是一個(gè)用suspend關(guān)鍵字修飾的一個(gè)無參,無返回值的函數(shù)類型。被suspend修飾的函數(shù)稱為掛起函數(shù),與之對(duì)應(yīng)的是關(guān)鍵字resume(恢復(fù)),suspend,對(duì)協(xié)程的掛起并沒有實(shí)際作用,其實(shí)只是一個(gè)提醒,函數(shù)創(chuàng)建者對(duì)函數(shù)的調(diào)用者的提醒,提醒調(diào)用者我是需要耗時(shí)操作,需要用掛起的方式,在協(xié)程中使用。

注意:掛起函數(shù)只能在協(xié)程中和其他掛起函數(shù)中調(diào)用,不能在其他地方使用,因?yàn)槠胀ê瘮?shù)沒有suspend和resume這兩個(gè)特性,所以必須要在協(xié)程的作用中使用。通過報(bào)錯(cuò)來提醒調(diào)用者和編譯器,這是一個(gè)耗時(shí)函數(shù),需要放在后臺(tái)執(zhí)行。

給函數(shù)前加上suspend 關(guān)鍵字

suspend fun testSuspendfun(){
     
  }

需要使用掛起函數(shù)常見的場(chǎng)景有:

  • 耗時(shí)操作:使用 withContext 切換到指定的 IO 線程去進(jìn)行網(wǎng)絡(luò)或者數(shù)據(jù)庫(kù)請(qǐng)求、io耗時(shí)操作獲取數(shù)據(jù)庫(kù)數(shù)據(jù)、一些等待一會(huì)需要的操作:列表排除,json解析等;

  • 等待操作:使用delay方法去等待某個(gè)事件。

八. 取消協(xié)程

現(xiàn)在我們已經(jīng)可以完整的運(yùn)行一個(gè)協(xié)程任務(wù)了,還有一個(gè)問題,就是如何取消協(xié)程呢?這個(gè)也很重要,比如Android中的網(wǎng)絡(luò)請(qǐng)求等資源數(shù)據(jù)的加載,需要在頁面關(guān)閉時(shí)中斷,從而減少性能流量的損耗,以及避免一些內(nèi)存泄露的問題。

//1.通過協(xié)程cancel()
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
  launch {
    while (true) {
      log("inner-launch")
    }
  }
  while (true) {
    log("outer-launch")
  }
}
scope.launch {
  delay(1000)
  scope.cancel()
}
//2.通過CorouinteScope.cancel)(
val scope = CoroutineScope(Dispatchers.IO)
val outerJob = scope.launch {
  launch {
    while (true) {
      log("inner-launch")
    }
  }
  while (true) {
    log("outer-launch")
  }
}
scope.launch {
  delay(1000)
  outerJob.cancel()
}

kotlin協(xié)程的取消規(guī)則是這樣的:

  • 父協(xié)程調(diào)用cancel(),會(huì)取消自己以及所有子(內(nèi)部)協(xié)程。
  • 子協(xié)程調(diào)用cancel(),默認(rèn)不會(huì)取消父協(xié)程。

可以通過調(diào)用CoroutineScope的cancel()方法,取消掉該scope產(chǎn)生的所有協(xié)程。

據(jù)此,以上兩個(gè)demo的行為是這樣的:

  1. outer和inner的協(xié)程全部被取消。
  2. outer和inner以及scope開啟的所有協(xié)程被取消。

但是,運(yùn)行上面的demo我們會(huì)發(fā)現(xiàn),log會(huì)一直輸出東西,這是為什么呢?因?yàn)閰f(xié)程的cancel()原理是改變了協(xié)程對(duì)象的內(nèi)部狀態(tài),但并沒有終止邏輯代碼的調(diào)用,也就是說協(xié)程狀態(tài)和代碼運(yùn)行是兩個(gè)部分,具體的原理我們?cè)谙旅鏁?huì)說。那我們應(yīng)該怎么辦呢?

答案很簡(jiǎn)單,既然改變了協(xié)程的狀態(tài),那么我們用協(xié)程狀態(tài)字段來判斷協(xié)程是否被取消了即可,將判斷條件代碼改成如下:

while (isActive) {
  log("outer-launch")
}

isActive是協(xié)程的一個(gè)狀態(tài)字段。

八. 異常捕獲

現(xiàn)在我們成功通過協(xié)程執(zhí)行了一段代碼,對(duì)于執(zhí)行代碼,必不可少就是對(duì)可能的異常進(jìn)行捕獲和處理。
kotlin的協(xié)程,也有一套自己的捕獲異常機(jī)制。

//1.根協(xié)程為launch
CoroutineScope(Dispatchers.IO).launch {
  async{ launch { throw IllegalStateException("this is an error") } }
}
//2.根協(xié)程為async
CoroutineScope(Dispatchers.IO).async {
  async{ launch { throw IllegalStateException("this is an error") } }
}
//3.捕獲async{}.await()異常
CoroutineScope(Dispatchers.IO).async {
  try {
    val job = async { throw IllegalStateException("this is an error") }
    val data = job.await()
    //do something with data
  } catch (e: Exception) {
    log(e.message)
  }
}

先來簡(jiǎn)單描述下協(xié)程的異常機(jī)制:

在協(xié)程內(nèi)部通過try-catch捕獲的異常,由我們自己處理(和java異常一樣)。

未捕獲的異常,協(xié)程本身默認(rèn)不處理,而是一層一層的交由父協(xié)程,直到根協(xié)程進(jìn)行處理。

根協(xié)程處理異常時(shí),會(huì)使用注冊(cè)的CoroutineExceptionHandler對(duì)象進(jìn)行處理;Android的協(xié)程依賴包,會(huì)引入并自動(dòng)注冊(cè)一個(gè)該對(duì)象,處理行為與java處理一致(直接交由UncaughtExceptionHandler)。

有些協(xié)程類型重寫了處理異常方法,默認(rèn)不處理異常,比如async式協(xié)程,這類協(xié)程作為根協(xié)程的話,最終會(huì)導(dǎo)致異常丟失,繼續(xù)執(zhí)行后續(xù)邏輯。

內(nèi)部協(xié)程出現(xiàn)異常,會(huì)逐層cancel掉父協(xié)程。

據(jù)此機(jī)制,我們看下上面三個(gè)demo的異常處理情況:

  • 根協(xié)程為launch式協(xié)程時(shí),會(huì)使用Android提供的handler進(jìn)行異常拋出,最終表現(xiàn)就是應(yīng)用崩潰。

  • 根協(xié)程為async式協(xié)程時(shí),不會(huì)處理異常,最終表現(xiàn)就是沒異常的拋出(但繼續(xù)執(zhí)行下去其實(shí)很危險(xiǎn))。

  • async式協(xié)程的await()方法,在返回異常時(shí),會(huì)進(jìn)行拋出,所以我們可以通過try-catch這個(gè)await()方法,來捕獲async式協(xié)程產(chǎn)生的異常。

這里需要注意的是,如果根協(xié)程為launch式協(xié)程,即使我們使用(3)描述的辦法,依然沒有辦法阻止崩潰,因?yàn)閘aunch協(xié)程會(huì)處理異常并拋出。

綜上所述,對(duì)于我們想捕獲的異常,最靠譜的辦法還是在協(xié)程內(nèi)部自己捕獲異常進(jìn)行處理,避免因?yàn)槲床东@而直接崩潰。

Kotlin 協(xié)程之一:基礎(chǔ)使用
破解 Kotlin 協(xié)程(1) - 入門篇
Kotlin Coroutine(協(xié)程) 簡(jiǎn)介
kotlin 協(xié)程在 Android 中的使用(Jetpack 中的協(xié)程、Retofit中使用協(xié)程)
kotlin協(xié)程-Android實(shí)戰(zhàn)
Kotlin Primer·第七章·協(xié)程庫(kù)
Kotlin協(xié)程學(xué)習(xí)
Kotlin Coroutines基礎(chǔ)和原理初探
kotlin極簡(jiǎn)教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容