一文讀懂kotlin協(xié)程常用知識(shí)點(diǎn)

先不講概念寒屯,先上代碼,看一下協(xié)程怎么用的黍少。

retrofit 請(qǐng)求代碼

interface HttpInterface {
    @GET("/photos/random")
    suspend fun getImageRandom(@Query("count") count: Number): ArrayList<ImageBean>
}

activity 中調(diào)用代碼

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        val imageArray: ArrayList<ImageBean> = httpInterface.getImageRandom(10)//發(fā)送請(qǐng)求
        textView.text = "圖片數(shù)量為" + imageArray.size//更新UI
    }
}

可以看到發(fā)送請(qǐng)求和更新 UI 在一個(gè)代碼塊中寡夹,看起來(lái)像是都運(yùn)行在主線程中,但是竟然沒有任何報(bào)錯(cuò)厂置。 這就是協(xié)程最有魅力的地方非阻塞式掛起菩掏,后邊會(huì)詳細(xì)介紹。

創(chuàng)建協(xié)程

創(chuàng)建協(xié)程有三種方式:launch昵济、async智绸、runBlocking

launch

launch 方法簽名如下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    //省略
    return coroutine
}

launch 是 CoroutineScope 的擴(kuò)展方法野揪,需要 3 個(gè)參數(shù)。第一個(gè)參數(shù)瞧栗,看字面意思是協(xié)程上下文斯稳,后邊會(huì)重點(diǎn)講到。第二個(gè)參數(shù)是協(xié)程啟動(dòng)模式沼溜,默認(rèn)情況下平挑,協(xié)程是創(chuàng)建后立即執(zhí)行的。第三個(gè)參數(shù)系草,官方文檔說這個(gè) block 就是協(xié)程代碼塊,所以是必傳的唆涝。返回的是一個(gè) Job找都,這個(gè) Job 可以理解為一個(gè)后臺(tái)工作,在 block 代碼塊執(zhí)行完成后會(huì)結(jié)束廊酣,也可以通過 Job 的 cancel 方法取消它能耻。

async

async 方法簽名如下:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    //省略
    return coroutine
}

同樣也是 CoroutineScope 的擴(kuò)展方法,參數(shù)跟 launch 是一模一樣的亡驰,只是返回參數(shù)變成了 Deferred晓猛,這個(gè) Deferred 繼承于 Job,相當(dāng)于一個(gè)帶返回結(jié)果的 Job凡辱,返回結(jié)果可以通過調(diào)用它的 await 方法獲取戒职。

runBlocking

runBlocking 會(huì)阻塞調(diào)用他的線程,直到代碼塊執(zhí)行完畢透乾。

Log.i("zx", "當(dāng)前線程1-" + Thread.currentThread().name)
runBlocking(Dispatchers.IO) {
    delay(2000)
    Log.i("zx", "休眠2000毫秒后洪燥,當(dāng)前線程" + Thread.currentThread().name)
}
Log.i("zx", "當(dāng)前線程2-" + Thread.currentThread().name)

輸出內(nèi)容

當(dāng)前線程1-main
休眠2000毫秒后,當(dāng)前線程DefaultDispatcher-worker-1
當(dāng)前線程2-main

可以看到乳乌,即使協(xié)程指定了運(yùn)行在 IO 線程捧韵,依舊會(huì)阻塞主線程。runBlocking 主要用來(lái)寫測(cè)試代碼汉操,平常不要隨意用再来,所以不再過多介紹。

CoroutineScope 協(xié)程作用域

launch 和 async 都是 CoroutineScope 的擴(kuò)展函數(shù)磷瘤,CoroutineScope 又是什么呢芒篷,字面意思翻譯過來(lái)是協(xié)程作用域,協(xié)程作用域類似于變量作用域膀斋,定義了協(xié)程代碼的作用范圍梭伐。作用域取消時(shí),作用域中的協(xié)程都會(huì)被取消仰担。 比如如下代碼:

MainScope().launch {
    var i = 0

    launch(Dispatchers.IO) {
        while (true) {
            Log.i("zx", "子協(xié)程正在運(yùn)行著$i")
            delay(1000)
        }
    }

    while (true) {
        i++
        Log.i("zx", "父協(xié)程正在運(yùn)行著$i")

        if (i>4) {
            cancel()
        }
        delay(1000)
    }
}

輸出:

父協(xié)程正在運(yùn)行著1
子協(xié)程正在運(yùn)行著1
父協(xié)程正在運(yùn)行著2
子協(xié)程正在運(yùn)行著2
父協(xié)程正在運(yùn)行著3
子協(xié)程正在運(yùn)行著3
父協(xié)程正在運(yùn)行著4
子協(xié)程正在運(yùn)行著4
子協(xié)程正在運(yùn)行著4
父協(xié)程正在運(yùn)行著5

5 秒后糊识,父協(xié)程調(diào)用 cancel()結(jié)束了绩社,子協(xié)程也就結(jié)束了,并沒有繼續(xù)打印出值。

可以通過 CoroutineScope()來(lái)創(chuàng)建協(xié)程作用域赂苗,這并不是一個(gè)構(gòu)造函數(shù)愉耙,CoroutineScope 是一個(gè)接口,所以沒有構(gòu)造函數(shù)拌滋,只是函數(shù)名與接口名同名而已朴沿,源碼如下:

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

源碼可見,創(chuàng)建 CoroutineScope 時(shí)需要傳入 CoroutineContext败砂,這個(gè) CoroutineContext 也是 CoroutineScope 接口中唯一的成員變量赌渣。CoroutineScope.kt 這個(gè)文件中使用 CoroutineScope()創(chuàng)建了兩個(gè) Scope,一個(gè)是 MainScope昌犹,一個(gè)是 GlobalScope坚芜。源碼如下:

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

MainScope 是一個(gè)方法,返回了一個(gè)運(yùn)行在主線程的作用域斜姥,需要手動(dòng)取消鸿竖。GlobalScope 是一個(gè)全局作用域,整個(gè)應(yīng)用程序生命周期他都在運(yùn)行铸敏,不能提前取消缚忧,所以一般不會(huì)使用這個(gè)作用域。Android 中杈笔,ktx 庫(kù)提供了一些常用的作用域供我們使用闪水,如 lifecycleScope 和 viewModelScope。在 LifecycleOwner 的所有實(shí)現(xiàn)類中桩撮,如 Activity 和 Fragment 中都可以直接使用 lifecycleScope敦第,lifecycleScope 會(huì)跟隨 Activity 或 Fragment 的生命周期,在 Activity 或 Fragment 銷毀時(shí)店量,自動(dòng)取消協(xié)程作用域中的所有協(xié)程芜果,不用手動(dòng)管理,不存在內(nèi)存泄露風(fēng)險(xiǎn)融师。類似的 viewModelScope 也會(huì)隨著 viewModel 的銷毀而取消右钾。

目前已經(jīng)有好幾個(gè)地方出現(xiàn)了 CoroutineContext:?jiǎn)?dòng)協(xié)程時(shí) launch 或者 async 方法需要 CoroutineContext,創(chuàng)建協(xié)程作用域時(shí)需要 CoroutineContext旱爆,協(xié)程作用域中有且只有一個(gè)成員變量也是 CoroutineContext舀射,如下源碼所示:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

如此看來(lái),CoroutineContext 必定很重要怀伦。

CoroutineContext 協(xié)程上下文

CoroutineContext 保存了協(xié)程的上下文脆烟,是一些元素的集合(實(shí)際并不是用集合 Set 去存儲(chǔ)),集合中每一個(gè)元素都有一個(gè)唯一的 key房待。通俗來(lái)講邢羔,CoroutineContext 保存了協(xié)程所依賴的各種設(shè)置驼抹,比如調(diào)度器、名稱拜鹤、異常處理器等等框冀。

CoroutineContext 源碼如下:

public interface CoroutineContext {

    public operator fun <E : Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R, Element) -> R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else
            context.fold(this) { acc, element ->
                //省略
            }

    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        //省略
    }
}

CoroutineContext 里有一個(gè)接口 Element,這個(gè) Element 就是組成 CoroutineContext 的元素敏簿,最重要的是 plus 操作符函數(shù)明也,這個(gè)函數(shù)可以把幾個(gè) Element 合并成為一個(gè) CoroutineContext,由于是操作符函數(shù)惯裕,所以可以直接用+調(diào)用温数。比如:

var ctx = Dispatchers.IO + Job()  + CoroutineName("測(cè)試名稱")
Log.i("zx", ctx.toString())

輸出

[JobImpl{Active}@31226a0, CoroutineName(測(cè)試名稱), Dispatchers.IO]

共有哪幾種元素呢?來(lái)看看 Element 的子類吧轻猖。Element 有這么幾個(gè)子類(子接口):Job帆吻、CoroutineDispatcher、CoroutineName咙边、CoroutineExceptionHandler。

Job

Job 可以簡(jiǎn)單理解為一個(gè)協(xié)程的引用次员,創(chuàng)建協(xié)程后會(huì)返回 Job 實(shí)例败许,可以通過 Job 來(lái)管理協(xié)程的生命周期。Job 是 CoroutineContext 元素的一種淑蔚,可以傳入 CoroutineScope 用來(lái)使協(xié)程有不同的特性市殷。主要關(guān)注Job()SupervisorJob()這兩個(gè)創(chuàng)建 Job 的函數(shù)以及Deferred這個(gè) Job 的子接口刹衫。

Job()

創(chuàng)建一個(gè)處于活動(dòng)狀態(tài)的 Job 對(duì)象醋寝,可以傳入父 Job,這樣當(dāng)父 Job 取消時(shí)就可以取消該 Job 以及他的子項(xiàng)带迟。 該 Job 的任何子項(xiàng)失敗都會(huì)立即導(dǎo)致該 Job 失敗音羞,并取消其其余子項(xiàng)。這個(gè)很好理解仓犬,例如:

CoroutineScope(Dispatchers.IO + Job()+MyExceptionHandler()).launch {
    var index = 0
    launch {
        while (true) {
            index++
            if (index > 3) {
                throw Exception("子協(xié)程1異常了")
            }
            Log.i("zx", "子協(xié)程1正在運(yùn)行")
        }
    }

    launch {
        while (true) {
            Log.i("zx", "子協(xié)程2正在運(yùn)行")
        }
    }
}

子協(xié)程 1 異常了嗅绰,就會(huì)導(dǎo)致整個(gè) Job 失敗,子協(xié)程 2 也不會(huì)繼續(xù)運(yùn)行搀继。

SupervisorJob()

創(chuàng)建一個(gè)處于活動(dòng)狀態(tài)的 Job 對(duì)象窘面。 該 Job 的子項(xiàng)之間彼此獨(dú)立,互不影響叽躯,子項(xiàng)的失敗或取消不會(huì)導(dǎo)致主 Job 失敗财边,也不會(huì)影響其他子項(xiàng)。

CoroutineScope(Dispatchers.IO + SupervisorJob() + MyExceptionHandler()).launch {
    launch {
        while (true) {
            index++
            if (index > 3) {
                throw Exception("子協(xié)程1異常了")
            }
            Log.i("zx", "子協(xié)程1正在運(yùn)行")
        }
    }

    launch {
        while (true) {
            Log.i("zx", "子協(xié)程2正在運(yùn)行")
        }
    }
}

同樣的代碼点骑,把 Job()換成 SupervisorJob()后酣难,可以發(fā)現(xiàn)子協(xié)程 2 會(huì)一直運(yùn)行谍夭,并不會(huì)因?yàn)樽訁f(xié)程 1 異常而被取消。

我們常見的 MainScope鲸鹦、viewModelScope慧库、lifecycleScope 都是用 SupervisorJob()創(chuàng)建的,所以這些作用域中的子協(xié)程異常不會(huì)導(dǎo)致根協(xié)程退出馋嗜。 kotlin 提供了一個(gè)快捷函數(shù)創(chuàng)建一個(gè)使用 SupervisorJob 的協(xié)程齐板,那就是 supervisorScope。例如:

CoroutineScope(Dispatchers.IO).launch {
    supervisorScope {
        //這里的子協(xié)程代碼異常不會(huì)導(dǎo)致父協(xié)程退出葛菇。
    }
}

等同于

CoroutineScope(Dispatchers.IO).launch {
    launch(SupervisorJob()) {

    }
}

Deferred

是 Job 的子接口甘磨,是一個(gè)帶有返回結(jié)果的 Job。async 函數(shù)創(chuàng)建的協(xié)程會(huì)返回一個(gè) Deferred眯停,可以通過 Deferred 的await()獲取實(shí)際的返回值济舆。async 與 await 類似于其他語(yǔ)言(例如 JavaScript)中的 async 與 await,通常用來(lái)使兩個(gè)協(xié)程并行執(zhí)行莺债。 例如如下代碼

suspend fun testAsync1(): String = withContext(Dispatchers.Default)
{
    delay(2000)
    "123"
}

suspend fun testAsync2(): String = withContext(Dispatchers.Default)
{
    delay(2000)
    "456"
}

lifecycleScope.launch {
    val time1 = Date()
    val result1 = testAsync1()
    val result2 = testAsync2()
    Log.i("zx", "結(jié)果為${result1 + result2}")
    Log.i("zx", "耗時(shí)${Date().time - time1.time}")
}

會(huì)輸出:

結(jié)果為123456
耗時(shí)5034

如果改為使用 async滋觉,讓兩個(gè)協(xié)程并行。代碼如下:

lifecycleScope.launch {
    val time1 = Date()
    val result1 = async { testAsync1() }
    val result2 = async { testAsync2() }
    Log.i("zx", "結(jié)果為${result1.await() + result2.await()}")
    Log.i("zx", "耗時(shí)${Date().time - time1.time}")
}

輸出

結(jié)果為123456
耗時(shí)3023

總耗時(shí)為兩個(gè)并行協(xié)程中耗時(shí)較長(zhǎng)的那個(gè)時(shí)間齐邦。

CoroutineDispatcher 調(diào)度器

指定了協(xié)程運(yùn)行的線程或線程池椎侠,共有 4 種。

  • Dispatchers.Main 運(yùn)行在主線程措拇,Android 平臺(tái)就是 UI 線程我纪,是單線程的。
  • Dispatchers.Default 默認(rèn)的調(diào)度器丐吓,如果上下文中未指定調(diào)度器浅悉,那么就是 Default。適合用來(lái)執(zhí)行消耗 CPU 資源的計(jì)算密集型任務(wù)券犁。它由 JVM 上的共享線程池支持术健。 默認(rèn)情況下,此調(diào)度器使用的最大并行線程數(shù)等于 CPU 內(nèi)核數(shù)族操,但至少為兩個(gè)苛坚。
  • Dispatchers.IO IO 調(diào)度器,使用按需創(chuàng)建的線程共享池色难,適合用來(lái)執(zhí)行 IO 密集型阻塞操作泼舱,比如 http 請(qǐng)求。此調(diào)度器默認(rèn)并行線程數(shù)為內(nèi)核數(shù)和 64 這兩個(gè)值中的較大者枷莉。
  • Dispatchers.Unconfined 不限于任何特定線程的協(xié)程調(diào)度器娇昙,不常用。

需要注意的是 Default 和 IO 都是運(yùn)行在線程池中笤妙,兩個(gè)子協(xié)程有可能是在一個(gè)線程中冒掌,有可能不是一個(gè)線程中噪裕。例如如下代碼:

CoroutineScope(Dispatchers.IO).launch {
    launch {
        delay(3000)
        Log.i("zx", "當(dāng)前線程1-" + Thread.currentThread().name)
    }

    launch {
        Log.i("zx", "當(dāng)前線程2-" + Thread.currentThread().name)
    }
}

輸出

當(dāng)前線程2-DefaultDispatcher-worker-2
當(dāng)前線程1-DefaultDispatcher-worker-5

所以,如果涉及線程的 ThreadLocal 數(shù)據(jù)時(shí)股毫,記得做處理膳音。

如果一不小心用錯(cuò)了 Dispatchers.Default 去發(fā) IO 請(qǐng)求會(huì)有什么后果呢?猜測(cè)結(jié)果:由于 Default 調(diào)度器并行線程數(shù)遠(yuǎn)小于 IO 調(diào)度器铃诬,IO 請(qǐng)求的一個(gè)特性就是等待時(shí)間很長(zhǎng)祭陷,而實(shí)際的處理時(shí)間很短,所以會(huì)造成大量請(qǐng)求處于等待分配線程的狀態(tài)中趣席,造成效率低下兵志。實(shí)際情況可以寫個(gè)程序測(cè)試一下,這里就不試了宣肚。

CoroutineName 協(xié)程名稱

傳入一個(gè) String 作為協(xié)程名稱想罕,一般用于調(diào)試時(shí)日志輸出,以區(qū)分不同的調(diào)度器霉涨。

CoroutineExceptionHandler 異常處理器

用于處理協(xié)程作用域內(nèi)所有未捕獲的異常按价。實(shí)現(xiàn) CoroutineExceptionHandler 接口就好了,代碼如下:

class MyExceptionHandler : CoroutineExceptionHandler {
    override val key: CoroutineContext.Key<*>
        get() = CoroutineExceptionHandler

    override fun handleException(context: CoroutineContext, exception: Throwable) {
        Log.i("zx", "${context[CoroutineName]}中發(fā)生異常,${exception.message}")
    }
}

然后用+拼接并設(shè)置給作用域笙瑟。

CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
    launch(CoroutineName("子協(xié)程1") + MyExceptionHandler()) {
        throw Exception("完蛋了俘枫,異常了")
    }
}

輸出內(nèi)容為

CoroutineName(父協(xié)程)中發(fā)生異常,完蛋了,異常了

不對(duì)呀逮走,明明是子協(xié)程 1 拋出的異常,為什么輸出的是父協(xié)程拋出的異常呢今阳?原來(lái)师溅,異常規(guī)則就是子協(xié)程會(huì)將異常一級(jí)一級(jí)向上拋,直到根協(xié)程盾舌。那什么是根協(xié)程呢墓臭?跟協(xié)程簡(jiǎn)單來(lái)講就是最外層協(xié)程,還有一個(gè)特殊的規(guī)則就是妖谴,使用 SupervisorJob 創(chuàng)建的協(xié)程也視為根協(xié)程窿锉。比如如下代碼:

CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
    launch(CoroutineName("子協(xié)程1") + MyExceptionHandler() + SupervisorJob()) {
        throw Exception("完蛋了,異常了")
    }
}

輸出內(nèi)容為

CoroutineName(子協(xié)程1)中發(fā)生異常,完蛋了膝舅,異常了

說起處理異常嗡载,大家肯定想到 try / catch,為什么有了 try / catch仍稀,協(xié)程里還要有一個(gè) CoroutineExceptionHandler 呢洼滚?或者說 CoroutineExceptionHandler 到底起什么作用,什么時(shí)候用 CoroutineExceptionHandler 什么時(shí)候用 try / catch 呢技潘?官方文檔是這么描述 CoroutineExceptionHandler 的用于處理未捕獲的異常遥巴,是用于全局“全部捕獲”行為的最后一種機(jī)制千康。 你無(wú)法從CoroutineExceptionHandler的異常中恢復(fù)。 當(dāng)調(diào)用處理程序時(shí)铲掐,協(xié)程已經(jīng)完成拾弃。,這段文字描述的很清楚了摆霉,這是全局(這個(gè)全局是指根協(xié)程作用域全局)的異常捕獲豪椿,是最后的一道防線,此時(shí)協(xié)程已經(jīng)結(jié)束斯入,你只能處理異常砂碉,而不能做其他的操作。舉個(gè)例子吧

CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
    val test = 5 / 0
    Log.i("zx", "即使異常了刻两,我也想繼續(xù)執(zhí)行協(xié)程代碼增蹭,比如:我要通知用戶,讓用戶刷新界面")
}

協(xié)程體中第一行 5/0 會(huì)拋出異常磅摹,會(huì)在 CoroutineExceptionHandler 中進(jìn)行處理滋迈,但是協(xié)程就會(huì)直接結(jié)束,后續(xù)的代碼不會(huì)再執(zhí)行户誓,如果想繼續(xù)執(zhí)行協(xié)程饼灿,比如彈出 Toast 通知用戶,這里就做不到了帝美。換成 try / catch 肯定就沒有問題了碍彭。

CoroutineScope(Dispatchers.IO + CoroutineName("父協(xié)程") + MyExceptionHandler()).launch {
    try {
        val test = 5 / 0
    } catch (e: Exception) {
        Log.i("zx", "我異常了")
    }
    Log.i("zx", "繼續(xù)執(zhí)行協(xié)程的其他代碼")
}

那既然如此,我直接把協(xié)程中所有代碼都放在 try / catch 里悼潭,不用 CoroutineExceptionHandler 不就行了庇忌?聽起來(lái)好像沒毛病,那我們就試試吧

inline fun AppCompatActivity.myLaunch(
    crossinline block: suspend CoroutineScope.() -> Unit
) {
    CoroutineScope(Dispatchers.IO).launch {
        try {
            block()
        } catch (e: Exception) {
            Log.e("zx", "異常了舰褪," + e.message)
        }
    }
}

做了一個(gè)封裝皆疹,只要是調(diào)用封裝的 myLaunch 函數(shù),那所有的協(xié)程代碼都被 try / catch 包著占拍,這肯定沒問題了吧略就。比如我這樣調(diào)用

myLaunch {
    val test = 5 / 0
}

程序沒崩,很好晃酒。換個(gè)代碼繼續(xù)調(diào)用

myLaunch {
    launch {
        val test = 5 / 0
    }
}

APP 崩了表牢,不對(duì)呀,這里最外層明明已經(jīng)包了一層 try / catch掖疮,怎么捕獲不到呢初茶?想一下之前協(xié)程拋異常的規(guī)則:子協(xié)程會(huì)將異常一級(jí)一級(jí)向上拋,直到根協(xié)程。這里用 launch 又新創(chuàng)建了一個(gè)子協(xié)程恼布,異常代碼運(yùn)行在子協(xié)程中螺戳,子協(xié)程直接把異常拋給了父協(xié)程,所以 try / catch 捕獲不到折汞。這里父協(xié)程又沒有指定異常處理器倔幼,所以就崩了。有人可能要抬杠了爽待,那我直接在子協(xié)程里 try / catch 不就不會(huì)崩了损同?確實(shí)不會(huì)崩了,這里你記住了加try / catch鸟款,那別的地方會(huì)不會(huì)忘了加呢膏燃。所以 CoroutineExceptionHandler 全作用域捕獲異常的優(yōu)勢(shì)就出來(lái)了。所以簡(jiǎn)單總結(jié)一下二者的區(qū)別和使用場(chǎng)景吧。

  • CoroutineExceptionHandler 以協(xié)程為作用域全局捕獲未處理異常,可以捕獲子協(xié)程的異常缩滨,捕獲到異常時(shí)椿每,協(xié)程就已經(jīng)結(jié)束了球散。適用于做最后的異常處理以保證不崩潰,比如用來(lái)記錄日志等。
  • try / catch 可以更加精細(xì)的捕獲異常,精確到一行代碼或者一個(gè)操作黍衙,無(wú)法捕獲子協(xié)程的異常,不會(huì)提前結(jié)束協(xié)程荠诬。適用于捕獲可以預(yù)知的異常琅翻。

以下是個(gè)人的心得,不一定正確柑贞,僅供參考望迎。

CoroutineExceptionHandler 適用于捕獲無(wú)法預(yù)知的異常。try / catch 適用于可以預(yù)知的異常凌外。 什么是可以預(yù)知的異常和不可預(yù)知的異常呢?舉個(gè)例子:你要往磁盤寫文件涛浙,可能會(huì)沒有權(quán)限康辑,也可能磁盤寫滿了,這些異常都是可以預(yù)知的轿亮,此時(shí)應(yīng)該用 try / catch疮薇。不可預(yù)知的異常就是指,代碼看起來(lái)沒毛病我注,但我不知道哪里會(huì)不會(huì)出錯(cuò)按咒,不知道 try / catch 該往哪里加,try / catch 有沒有少加但骨,這個(gè)時(shí)候就該交給 CoroutineExceptionHandler励七,畢竟 CoroutineExceptionHandler 是最后一道防線智袭。

CoroutineContext 總結(jié)

CoroutineContext 由 Job、CoroutineDispatcher掠抬、CoroutineName吼野、CoroutineExceptionHandler 組成。Job 可以控制協(xié)程的生命周期两波,也決定了子項(xiàng)異常時(shí)瞳步,父Job會(huì)不會(huì)取消。CoroutineDispatcher決定了協(xié)程運(yùn)行在哪個(gè)線程腰奋。CoroutineName給協(xié)程起名字单起,用于調(diào)試時(shí)區(qū)分。CoroutineExceptionHandler 用于全作用域捕獲并處理異常劣坊。子協(xié)程會(huì)自動(dòng)繼承父協(xié)程的CoroutineContext嘀倒,并可以覆蓋。CoroutineContext元素之間可以通過 + 運(yùn)算符組合讼稚,也可以通過對(duì)應(yīng)的key檢索出CoroutineContext中的元素括儒。

CoroutineStart 啟動(dòng)模式

上邊講了 launch 和 async 的第二個(gè)參數(shù)就是 CoroutineStart,也就是協(xié)程的啟動(dòng)模式锐想,共分為如下 4 種:

  • DEFAULT-默認(rèn)模式帮寻,立即調(diào)度協(xié)程;

  • LAZY-僅在需要時(shí)才懶惰地啟動(dòng)協(xié)程,使用start()啟動(dòng)赠摇;

  • ATOMIC-原子地(以不可取消的方式)調(diào)度協(xié)程固逗,執(zhí)行到掛起點(diǎn)之后可以被取消;

  • UNDISPATCHED-同樣是原子地(以不可取消的方式)執(zhí)行協(xié)程到第一個(gè)掛起點(diǎn)藕帜。與ATOMIC的區(qū)別是:UNDISPATCHED不需要調(diào)度烫罩,直接執(zhí)行的,而ATOMIC是需要調(diào)度后再執(zhí)行的洽故;UNDISPATCHED是在父協(xié)程指定的線程中執(zhí)行贝攒,到達(dá)掛起點(diǎn)之后會(huì)切到自己上下文中指定的線程,ATOMIC是在自己的協(xié)程上下文中指定的線程執(zhí)行时甚。

    需要注意的是調(diào)度(schedules)和執(zhí)行(executes)是不一樣的隘弊,調(diào)度之后并不一定是立即執(zhí)行。

分別舉例說明荒适。

LAZY 模式:

val job = lifecycleScope.launch(start = CoroutineStart.LAZY) {
    Log.i("zx", "協(xié)程運(yùn)行了1")
}

上邊的代碼梨熙,并不會(huì)打印出內(nèi)容,需要手動(dòng)調(diào)用job.start(),才能啟動(dòng)協(xié)程并打印出內(nèi)容刀诬。

ATOMIC 模式:

val job = lifecycleScope.launch(start = CoroutineStart.ATOMIC) {
    Log.i("zx", "協(xié)程運(yùn)行了1")
    delay(2000)
    Log.i("zx", "協(xié)程運(yùn)行了2")
}
job.cancel()

由于使用的 ATOMIC 啟動(dòng)模式咽扇,執(zhí)行到掛起點(diǎn)之前(delay 是掛起函數(shù))是不能被取消的,所以無(wú)論如何都會(huì)打印出 "協(xié)程運(yùn)行了 1"。執(zhí)行到掛起點(diǎn)之后可以被取消质欲,所以不會(huì)打印出第二行树埠。

UNDISPATCHED 模式:

lifecycleScope.launch {
    Log.i("zx", "父協(xié)程,當(dāng)前線程" + Thread.currentThread().name)

    val job = launch(Dispatchers.IO, CoroutineStart.UNDISPATCHED) {
        Log.i("zx", "子協(xié)程把敞,當(dāng)前線程" + Thread.currentThread().name)
        delay(1000)
        Log.i("zx", "子協(xié)程delay后弥奸,當(dāng)前線程" + Thread.currentThread().name)
    }
}

上述代碼輸出

父協(xié)程,當(dāng)前線程main

子協(xié)程奋早,當(dāng)前線程main

子協(xié)程delay后盛霎,當(dāng)前線程DefaultDispatcher-worker-1

結(jié)果驗(yàn)證了,在到達(dá)第一個(gè)掛起點(diǎn)之前耽装,都是使用父協(xié)程所在線程去執(zhí)行協(xié)程愤炸,到達(dá)掛起點(diǎn)之后才會(huì)使用自己 coroutineContext 中設(shè)置的線程。類似于 ATOMIC 掉奄,在到達(dá)第一個(gè)掛起點(diǎn)之前同樣是不可取消的规个。

suspend 與 withContext

前邊反復(fù)提到掛起點(diǎn),那什么是掛起點(diǎn)呢姓建?什么又是掛起呢诞仓?掛起點(diǎn)實(shí)際上就是協(xié)程代碼執(zhí)行到 suspend 函數(shù)時(shí)的點(diǎn),此時(shí)協(xié)程會(huì)暫停速兔,suspend 函數(shù)之后的代碼不會(huì)再執(zhí)行墅拭,等到 suspend 函數(shù)執(zhí)行完之后,協(xié)程代碼會(huì)自動(dòng)繼續(xù)執(zhí)行涣狗。上邊用到的 delay 函數(shù)就是一個(gè)掛起函數(shù)谍婉,他會(huì)暫停(suspend)當(dāng)前協(xié)程代碼塊,先執(zhí)行 delay 函數(shù)镀钓,等 delay 執(zhí)行完后繼續(xù)執(zhí)行原有的代碼穗熬。先暫停,等代碼執(zhí)行完了在再自動(dòng)恢復(fù)(resume)執(zhí)行這個(gè)特性非常適合處理異步任務(wù)丁溅。例如如下代碼:

private suspend fun getBitmapByHttp(): Bitmap {
    Log.i("zx", "當(dāng)前線程" + Thread.currentThread().name)
    val url = URL("https://www.baidu.com/img/flexible/logo/pc/result.png");
    val imageConnection = url.openConnection() as HttpURLConnection
    imageConnection.requestMethod = "GET"
    imageConnection.connect()
    val inputStream: InputStream = imageConnection.inputStream
    return BitmapFactory.decodeStream(inputStream)
}

lifecycleScope.launch {
    val bitmap = getBitmapByHttp()//第一個(gè)行
    viewBinding.imageView.setImageBitmap(bitmap)//第二行
}

先定義了一個(gè) suspend 函數(shù)唤蔗,這個(gè)函數(shù)從網(wǎng)絡(luò)加載圖片獲取到 bitmap。然后啟動(dòng)一個(gè) lifecycleScope 的協(xié)程窟赏,在里邊調(diào)用這個(gè) suspend 函數(shù)措译。應(yīng)該如我們所想,第一行是個(gè) suspend 函數(shù)饰序,是個(gè)掛起點(diǎn),會(huì)等到 getBitmapByHttp 執(zhí)行完再繼續(xù)執(zhí)行第二行 setImageBitmap规哪。然而運(yùn)行起來(lái)之后求豫,先是輸出 "當(dāng)前線程 main" 然后應(yīng)用崩了,拋出了 NetworkOnMainThreadException 異常,為什么這里的 suspend 函數(shù)會(huì)運(yùn)行在主線程呢蝠嘉?因?yàn)?suspend 并不知道具體要切到哪個(gè)線程最疆,所以依舊運(yùn)行在主線程。并且上述代碼蚤告,Android Studio 會(huì)提示 Redundant 'suspend' modifier(多于的 suspend 修飾符)努酸。如何讓 suspend 函數(shù)切換到具體的線程呢?這就要用到 withContext 了杜恰。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

這是 withContext 的簽名获诈,可以看到 withContext 必須要傳入?yún)f(xié)程上下文以及一個(gè)協(xié)程代碼塊。協(xié)程上下文中包含了 Dispatchers心褐,它指定了 withContext 將要切到哪個(gè)線程中去執(zhí)行舔涎。withContext 也是一個(gè) suspend 掛起函數(shù),所以 withContext 執(zhí)行時(shí)逗爹,調(diào)用它的協(xié)程會(huì)先暫停亡嫌,等到它切到指定的線程并執(zhí)行完之后,會(huì)自動(dòng)再切回到調(diào)用它的協(xié)程掘而,并繼續(xù)執(zhí)行協(xié)程代碼挟冠。這其實(shí)就是掛起,自動(dòng)切走袍睡,執(zhí)行完了再自動(dòng)切回來(lái)繼續(xù)之前的操作知染。同樣是之前的代碼,加上 withContext 之后就沒問題了女蜈。

private suspend fun getBitmapByHttp(): Bitmap = withContext(Dispatchers.IO) {
    Log.i("zx", "當(dāng)前線程" + Thread.currentThread().name)
    val url = URL("https://www.baidu.com/img/flexible/logo/pc/result.png");
    val imageConnection = url.openConnection() as HttpURLConnection
    imageConnection.requestMethod = "GET"
    imageConnection.connect()
    val inputStream: InputStream = imageConnection.inputStream
    BitmapFactory.decodeStream(inputStream)
}

lifecycleScope.launch {
    val bitmap = getBitmapByHttp()
    viewBinding.imageView.setImageBitmap(bitmap)
}

既然 withContext 可以切走再切回來(lái)持舆,那調(diào)用時(shí)不要最外層的 lifecycleScope.launch {},不啟動(dòng)協(xié)程可以嗎伪窖。試了一下發(fā)現(xiàn) AS 提示錯(cuò)誤逸寓,編譯都過不了,提示"Suspend function 'getBitmapByHttp' should be called only from a coroutine or another suspend function"覆山,意思是掛起函數(shù)只能在另一個(gè)掛起函數(shù)或者協(xié)程里調(diào)用竹伸,那另一個(gè)掛起函數(shù)也只能在另另一個(gè)掛起函數(shù)或者協(xié)程里調(diào)用,如此套娃簇宽,最終就是掛起函數(shù)只能在一個(gè)協(xié)程里調(diào)用勋篓,這么限制是因?yàn)闀和!⑶凶呶焊睢⑶谢厝ゲ⒒謴?fù)執(zhí)行這些操作是由協(xié)程框架完成的譬嚣,如果不在協(xié)程里運(yùn)行,這些是沒法實(shí)現(xiàn)的钞它。

如果某個(gè)函數(shù)比較耗時(shí)拜银,我們就可以將其定義為掛起函數(shù)殊鞭,用 withContext 切換到非 UI 線程去執(zhí)行,這樣就不會(huì)阻塞 UI 線程尼桶。上邊的例子也展示了自定義一個(gè)掛起函數(shù)的過程操灿,那就是給函數(shù)加上 suspend 關(guān)鍵字,然后用 withContext 等系統(tǒng)自帶掛起函數(shù)將函數(shù)內(nèi)容包起來(lái)泵督。

試想一下趾盐,如果不用 suspend 和 withContext,那我們就需要自己寫開啟 Thread小腊,并自己用 Handler 去實(shí)現(xiàn)線程間通信救鲤。有了協(xié)程之后溢豆,這些都不需要我們考慮了,一下簡(jiǎn)單了很多麸折,更重要的是,這樣不會(huì)破壞代碼的邏輯結(jié)構(gòu),兩行代碼之間就像普通阻塞式代碼一樣,但是卻實(shí)現(xiàn)了異步非阻塞式的效果霉颠,這也就是非阻塞式的含義

小總結(jié):

  • 掛起 就是一個(gè)切走再自動(dòng)切回來(lái)繼續(xù)執(zhí)行的線程調(diào)度操作,這個(gè)操作由協(xié)程提供不从,所以限制了suspend方法只能在協(xié)程里調(diào)用条舔。
  • withContext 就是協(xié)程提供的一個(gè)掛起函數(shù)凄硼,起到的就是切到指定線程執(zhí)行代碼塊说墨,執(zhí)行完再切回來(lái)的作用突颊。
  • suspend 僅僅只是一個(gè)限制治唤,限制了掛起函數(shù)只能在協(xié)程中調(diào)用棒动,并沒有實(shí)際的切線程
  • 非阻塞式 寫法像普通阻塞式代碼一樣,卻實(shí)現(xiàn)了非阻塞式的效果宾添,沒有回調(diào)也沒有嵌套船惨,不破壞代碼邏輯結(jié)構(gòu)
  • 自定義掛起函數(shù) 給函數(shù)加上suspend關(guān)鍵字并用withContext等系統(tǒng)自帶掛起函數(shù)將函數(shù)內(nèi)容包起來(lái)

簡(jiǎn)單使用

就像文章一開始那樣柜裸,就可以簡(jiǎn)單使用協(xié)程+Retrofit 發(fā)送異步網(wǎng)絡(luò)請(qǐng)求了,但是沒有異常處理粱锐,我們可以簡(jiǎn)單封裝一下加上異常處理以及 loading 顯示等疙挺。

全局協(xié)程異常處理

class GlobalCoroutineExceptionHandler(
    val block: (context: CoroutineContext, exception: Throwable) -> Unit
) :
    CoroutineExceptionHandler {
    override val key: CoroutineContext.Key<*>
        get() = CoroutineExceptionHandler

    override fun handleException(context: CoroutineContext, exception: Throwable) {
        block(context, exception)
    }
}

這里的 handleException 并沒有實(shí)際處理異常,實(shí)際處理異常的方法是外邊初始化 CoroutineExceptionHandler 時(shí)傳進(jìn)來(lái)的block怜浅。

Http 請(qǐng)求 Activity 基類

open class HttpActivity : AppCompatActivity() {
    val httpInterface: HttpInterface = RetrofitFactory.httpInterface
    private var progress: ProgressDialog? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    inline fun launchMain(
        crossinline block: suspend CoroutineScope.() -> Unit
    ) {
        val job = lifecycleScope.launch(GlobalCoroutineExceptionHandler(::handException)) {
            showProgress()
            block()
        }

        job.invokeOnCompletion {
            hideProgress()
        }
    }

    fun showProgress() {
        if (progress == null) {
            progress = ProgressDialog.show(
                this, "", "加載中,請(qǐng)稍后...", false, true
            )
        }
    }

    fun hideProgress() {
        if (progress != null && progress!!.isShowing) {
            progress!!.dismiss()
            progress = null
        }
    }

    open fun handException(context: CoroutineContext, exception: Throwable) {
        var msg = ""
        if (exception is HttpException) {
            msg = when (exception.code()) {
                404 -> {
                    "$localClassName-異常了,請(qǐng)求404了铐然,請(qǐng)求的資源不存在"
                }

                500 -> {
                    "$localClassName-異常了,請(qǐng)求500了,內(nèi)部服務(wù)器錯(cuò)誤"
                }
                500 -> {
                    "$localClassName-異常了,請(qǐng)求401了恶座,身份認(rèn)證不通過"
                }
                else -> {
                    "$localClassName-http請(qǐng)求異常了,${exception.response()}"
                }

            }
        } else {
            msg = "$localClassName-異常了,${exception.stackTraceToString()}"

        }
        Log.e("zx", msg)

        hideProgress()
        Snackbar.make(
            window.decorView,
            msg,
            Snackbar.LENGTH_LONG
        )
            .show()
    }
}

定義了一個(gè) launchMain 函數(shù)搀暑,launchMain 中統(tǒng)一開啟協(xié)程,統(tǒng)一設(shè)置 CoroutineExceptionHandler跨琳,并會(huì)在請(qǐng)求時(shí)顯示環(huán)形進(jìn)度條自点,請(qǐng)求結(jié)束后隱藏進(jìn)度條。

定義了一個(gè) handException 函數(shù)脉让,這個(gè)函數(shù)是實(shí)際處理異常的函數(shù)桂敛,處理了一些常見的異常,并通過 Snackbar 顯示出來(lái)侠鳄。這里用的繼承埠啃,你也可以用擴(kuò)展函數(shù)去實(shí)現(xiàn)。

使用時(shí)讓 Activity 繼承 HttpActivity伟恶,然后就直接使用了,如果想自己處理異常碴开,實(shí)現(xiàn)handException函數(shù)就可以了。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    launchMain {
        val result = httpInterface.searchPhotos("china")
        //更新UI
    }
}

當(dāng)然這只是一種簡(jiǎn)單的用法博秫,沒有結(jié)合 ViewModel 和 LiveData潦牛,如果你不需要 MVVM,僅僅只需要在 Activity 中發(fā)請(qǐng)求挡育,或許可以考慮使用這種方式巴碗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市即寒,隨后出現(xiàn)的幾起案子橡淆,更是在濱河造成了極大的恐慌,老刑警劉巖母赵,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逸爵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡凹嘲,警方通過查閱死者的電腦和手機(jī)师倔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)周蹭,“玉大人趋艘,你說我怎么就攤上這事疲恢。” “怎么了瓷胧?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵显拳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我搓萧,道長(zhǎng)萎攒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任矛绘,我火速辦了婚禮,結(jié)果婚禮上刃永,老公的妹妹穿的比我還像新娘货矮。我一直安慰自己,他們只是感情好斯够,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布囚玫。 她就那樣靜靜地躺著,像睡著了一般读规。 火紅的嫁衣襯著肌膚如雪抓督。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天束亏,我揣著相機(jī)與錄音铃在,去河邊找鬼。 笑死碍遍,一個(gè)胖子當(dāng)著我的面吹牛定铜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播怕敬,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼揣炕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了东跪?” 一聲冷哼從身側(cè)響起畸陡,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虽填,沒想到半個(gè)月后丁恭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卤唉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年涩惑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桑驱。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡竭恬,死狀恐怖跛蛋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情痊硕,我是刑警寧澤赊级,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站岔绸,受9級(jí)特大地震影響理逊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盒揉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一晋被、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧刚盈,春花似錦羡洛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至肋联,卻和暖如春威蕉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背橄仍。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工韧涨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侮繁。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓氓奈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親鼎天。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舀奶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容