手摸手帶你走進Kotlin Coroutine

參考文章

kotlin官網

kotlin github

Roman Elizarov 視頻(基於1.3之前的,一些用法已經改變,但是主要看原理和思想)

Roman Elizarov 視頻深入(基於1.3之前的,一些用法已經改變,但是主要看原理和思想)

Roman Elizarov 基於1.3講解

jakeWharton 的適配Retrofit的adapter

Coroutine的優勢

  • 簡化異步編程,支持異步返回
  • 掛起不阻塞線程,提高線程利用率

怎樣簡單使用協程

首先,我們考慮一個問題:一個進程中支持多少個線程呢?

在Roman Elizarov的視頻中說道,一個普通的手機大概可能運行1000個線程(已經很勉強了),在張紹文的優化課程中說道一個進程大概支持400左右個線程,接下來我們做個試驗:

 fun main() {
        repeat(10000) {
            Thread {
                print(".")
            }.start()
        }
    }
    //會在手機上拋出OutOfMemoryError的錯誤
    
    fun main() = runBlocking {
       repeat(10000){
           launch {
               print(".")
           }
       }
    }
    //正常打印

這個結果可以說明,同樣是在異步線程打印,協程就是比較輕量級的,別說10000個再增加10倍也沒得問題,是不是很神奇。
ps:在honor8 上實驗了,大概可以開啓9000個左右的線程,當然是在這個應用只有MainActivity的情況下。

異步線程的開發

callBack我們已經使用了很多年,callBack最大的一個問題就是“迷之縮進”,還有就是當一個線程網絡請求的時候,是需要等待的,等待是不釋放資源。

fun postItem(item :Item){
    requestTokenAsync {token ->
        createPostAsync(token, item) {post ->
            processPost(post)
        }
    }
}

解決方案:Futures/Promises/Rx

fun postItem(item:Item){
    requestTokenAsync()
    .thenCompose{token -> createPostAsync(token, item)}
    .thenAccept{post -> processPost(post)}
}

coroutine來拯救世界

suspend fun postItem(item:Item){
    val token = async{requestToken()}
    val post = async{createPost(token.await(), item)}
    processPost(post.await())
}

main(){
    GlobalScope.launch{
        ....
    }
}

看起是不是很舒服,少了一大堆的回調函數,suspend讓我們可以將一個異步請求的函數,當成一個普通的函數,再也不需要各種回調,各種鏈式調用。

到此爲止,如果只是想簡單嚐鮮使用,那看到這裏就可以了,如果想繼續瞭解,就需要往下看。

協程基礎

怎麼樣寫一個協程

fun main(){
    GlobalScope.launch {//在後臺啓動一個新的協程並開始
        delay(1000l)//無阻塞等待1秒鐘
        println("world")//延遲後輸出字符
    }
    println("Hello ")
    Thread.sleep(2000l)//阻塞主線程,保證主線程活着
}

//輸出
Hello 
world

本質上,協程是輕量級的線程。他們在CoroutineScope上下文中和launch協程構建器一起被啓動。這裏我們在GlobalScope中啓動一個新的協程,存活時間是指新的協程的存活時間被限制在了整個應用程序的存活時間之內。

你可以使用一些協程操作來替換一些線程操作,比如:用GlobalScope.launch{...}替換thread{...}delay(...)替換Thread.sleep(...)

這裏需要注意下,suspend關鍵字和delay(...)只能用在協程中。

協程中實現阻塞和非阻塞線程

在協程中使用GlobalScope.launch{...}表示非阻塞的協程,使用runBlocking{...}來表示阻塞的協程

fun main() {
    GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主線程中的代碼會立即執行
    runBlocking {     // 但是這個函數阻塞了主線程
        delay(2000L)  // ……我們延遲2秒來保證 JVM 的存活
    } 
}
//輸出
Hello,
World!

我們也可以使用runBlocking{...}來包裝函數,例如上面的例子可以寫爲:

fun main() = runBlocking<Unit> { // 開始執行主協程
    GlobalScope.launch { // 在後臺開啓一個新的協程並繼續
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主協程在這裏會立即執行
    delay(2000L)      // 延遲2秒來保證 JVM 存活
}

等待一個任務

使用延時(sleep或者delay)不是一個很好的選擇。使用非阻塞的方式來等待Job結束

val job = GlobalScope.launch{//啓動一個新的協程保持對這個任務的引用
    delay(1000l)
    println("World!")
}
println("Hello,")
jon.join()//等待知道子協程執行結束
//輸出
Hello,
World!

結構化的併發

這裏還有一些東西我們期望的寫法被使用在協程的實踐中。當我們使用GlobalScope.launch時我們創建了一個最高優先級的協程。甚至,雖然它是輕量級的,但當它運行起來仍然消耗了一些內存資源。甚至如果我們時序一個對新創建協程的引用,它任然會繼續運行。如果一段代碼在協程中掛起(舉例說明,我們錯誤的延遲了太長的時間),如果我們啓動了太多的協程,是否會導致內存溢出?如果我們手動引用所有的協程和join是非常容易出錯的。

一個好的解決方案:我們可以在代碼中使用結構併發。用來答題在GlobalScope中啓動協程,就像我們使用線程那樣(線程總是全局的),我們可以在一個具體的作用域中啓動協程並操作。

在上面的例子中,我們有一個轉換成使用runBlocking的協程構建器main函數,每一個協程構建器,包括runBlocking,在它代碼塊的作用域添加一個CoroutineScope實例。在這個作用域內啓動的協程不需要明確的調用join,因爲一個外圍的協程(我們的例子中的runBlocking)只有在它作用域內所有協程執行完畢之後纔會結束。從而,我們修改一下上面的例子:

 fun main() = runBlocking {//CoroutineScope
        launch { // 在runBlocking作用域中啓動一個新協程
            delay(1000)
            println("world!")
        }
        println("hello,")
    }
//輸出
Hello,
World!

作用域構建

除了由上面多種構建器提供的協程作用域,也可以使用coroutineScope構建起來生命自己的作用域。它啓動了一個新的協程作用域並且在所有子協程執行結束後纔會執行完畢。runBlockingcoroutineScope主要的不同之處在於後者在等待所有的子協程執行完畢時候並沒有使當前的線程阻塞。

fun main() = runBlocking { // this: CoroutineScope
        launch {
            delay(200L)
            println("Task from runBlocking :${Thread.currentThread().name}")
        }

        coroutineScope { // 創建一個新的協程作用域
            launch {
                delay(500L)
                println("Task from nested launch :${Thread.currentThread().name}")
            }

            delay(100L)
            println("Task from coroutine scope :${Thread.currentThread().name}") // 該行將在嵌套啓動之前執行打印
        }

        println("Coroutine scope is over :${Thread.currentThread().name}") // 該行將在嵌套結束之後纔會被打印
    }
    
    //輸出
    Task from coroutine scope :main
    Task from runBlocking :main
    Task from nested launch :main
    Coroutine scope is over :main

提取函數重構

讓我們在launch{...}中提取代碼塊並分離到另一個函數中。當你在這段代碼上展示提取函數的時候,我們需要使用到suspend關鍵字。表示這個函數式掛起的函數。

協程是輕量級的

fun main() = runBlocking {
        repeat(100000){//啓動大量的協程
            launch {
                print(".")
            }
        }
    }

它啓動了100,000個協程,並且每個協程打印一個點。 現在,嘗試使用線程來這麼做。將會發生什麼?(大多數情況下你的代碼將會拋出內存溢出錯誤)

全局協程類似守護線程

下面的代碼在GlobalScope中啓動了一個長時間運行的協程,它在1s內打印了"I’m sleep",然後延遲以一段時間

fun main() = runBlocking {
        GlobalScope.launch {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        }
        delay(1300L) // 在延遲之後結束程序
    }
    //輸出
    I'm sleeping 0 ...
    I'm sleeping 1 ...
    I'm sleeping 2 ...

在 GlobalScope 中啓動的活動中的協程就像守護線程一樣,不能使它們所在的進程保活。

取消與超時

取消協程的執行

在一個長時間運行的應用程序中,也許需要對後臺協程進行粒度的控制。比如說,一個用戶也許關閉了一個啓動協程的界面,那麼現在協程執行結果已經不再被需要了,這時,它應該是可以被取消的。該launch函數返回了一個可以被用來取消運行中的協程的Job

fun main() = runBlocking {
        val job = GlobalScope.launch {
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        }
        delay(1300L) // 在延遲之後結束程序
        println("I'm tired of waiting")
        job.cancel()
        job.join()
        println("Now I can quit.")
    }
    //輸出
    I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm tired of waiting
Now I can quit.

一旦main函數調用了job.cancel,我們在其他的協程中就看不到任何輸出了,因爲它被取消了。這裏也有一個可以使Job掛起的函數cancelAndJoin它合併了對cancel以及join的調用

取消是協作(cooperative)的

協程的取消是協作(cooperative)的,什麼意思呢?就是一段協程代碼必須協作才能被取消。所有kotlinx.coroutines中的掛起都是可以被徐橋的。它們檢查協程的取消,並在取消時拋出CancellationException。然而,如果協程正在執行計算任務,並沒有檢查取消的話,那麼它是不能被取消的,例如:

val startTime = System.currentTimeMillis();
    fun main() = runBlocking {
        val job = GlobalScope.launch {
            var nextPrintTime = startTime
            var i = 0
            while (i < 10) { // 一個執行計算的循環,只是爲了佔用CPU
                // 每秒打印消息兩次
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }
        delay(1300L) // 在延遲之後結束程序
        println("I'm tired of waiting")
        job.cancelAndJoin()
        println("Now I can quit.")
    }
    //輸出
    I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm tired of waiting
I'm sleeping 3 ...
I'm sleeping 4 ...
I'm sleeping 5 ...
I'm sleeping 6 ...
I'm sleeping 7 ...
I'm sleeping 8 ...
I'm sleeping 9 ...
Now I can quit.

我們可以看到連續打印出了"I’m sleeping",甚至在調用取消後,任務仍然執行了10次循環才結束。

使計算代碼可以取消

我們有兩種方法來使執行計算的代碼可以被取消。第一種方法是定期調用掛起函數來檢查取消。對於這種目的 yield 是一個好的選擇。 另一種方法是顯式的檢查取消狀態。讓我們試試第二種方法。

將前一個示例中的 while (i < 5) 替換爲 while (isActive) 並重新運行它。

val startTime = System.currentTimeMillis();
    fun main() = runBlocking {
        val job = GlobalScope.launch {
            var nextPrintTime = startTime
            var i = 0
            while (i < 10) { // 一個執行計算的循環,只是爲了佔用CPU
                // 每秒打印消息兩次
                if (isActive) {
                    println("I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            }
        }
        delay(1300L) // 在延遲之後結束程序
        println("I'm tired of waiting")
        job.cancelAndJoin()
        println("Now I can quit.")
    }
     //輸出
    I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
I'm tired of waiting
Now I can quit.

isActive是一個可以被使用在CoroutineScope中的擴展函數

在finally中釋放資源

我們通常使用如下的方式來處理在被取消時拋出CancellationException的可被取消的掛起函數。比如說,try{...}finally{...}表達式以及Kotlin的user函數一般在協程被取消的時候執行它們的終結動作

val job = launch {
    try {
        repeat(1000) { i ->
                println("I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        println("I'm running finally")
    }
}
delay(1300L) // 延遲一段時間
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消該任務並且等待它結束
println("main: Now I can quit.")

//輸出
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
main: I'm tired of waiting!
I'm running finally
main: Now I can quit.

運行不能取消的代碼塊

在前一個例子中任何嘗試在 finally 塊中調用掛起函數的行爲都會拋出 CancellationException,因爲這裏持續運行的代碼是可以被取消的。通常,這並不是一個問題,所有良好的關閉操作(關閉一個文件、取消一個任務、或是關閉任何一種通信通道)通常都是非阻塞的,並且不會調用任何掛起函數。然而,在真實的案例中,當你需要掛起一個被取消的協程,你可以將相應的代碼包裝在 withContext(NonCancellable) {……} 中,並使用 withContext 函數以及 NonCancellable 上下文,見如下示例所示:

fun main() = runBlocking {
        val job = GlobalScope.launch {
            try {
                repeat(1000) { i ->
                    println("I'm sleeping $i ...")
                    delay(500L)
                }
            } finally {
                withContext(NonCancellable) {
                    println("I'm running finally")
                    delay(1000L)
                    println("And I've just delayed for 1 sec because I'm non-cancellable")
                }
            }
        }
        delay(1300L) // 在延遲之後結束程序
        println("I'm tired of waiting")
        job.cancelAndJoin()
        println("Now I can quit.")
    }

超時

在實踐中絕大多數取消一個協程理由可能是超時。當你手動追蹤一個相關Job的引用並驅動了一個單獨的協程在延遲後取消追蹤,這裏已經準備好使用withTimeout函數來做這件事

fun main() = runBlocking {
        withTimeout(1300){
            repeat(1000) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
        }
    }
    
    //輸出
    I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

withTimeout 拋出了 TimeoutCancellationException,它是 CancellationException 的子類。 我們之前沒有在控制檯上看到堆棧跟蹤信息的打印。這是因爲在被取消的協程中 CancellationException 被認爲是協程執行結束的正常原因。 然而,在這個示例中我們在 main 函數中正確地使用了 withTimeout。

由於取消只是一個例外,所有的資源都使用常用的方法來關閉。 如果你需要做一些各類使用超時的特別的額外操作,可以使用類似 withTimeout 的 withTimeoutOrNull 函數,並把這些會超時的代碼包裝在 try {…} catch (e: TimeoutCancellationException) {…} 代碼塊中,而 withTimeoutOrNull 通過返回 null 來進行超時操作,從而替代拋出一個異常:

fun main() = runBlocking {
        val result = withTimeoutOrNull(1300L) {
            repeat(3) { i ->
                println("I'm sleeping $i ...")
                delay(500L)
            }
            "Done" // 在它運行得到結果之前取消它
        }
        println("Result is $result")
    }
    //輸出
    I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

組合掛起函數

默認順序調用

在不同地方定義兩個進行某種調用遠程服務或者進行計算掛起的函數。我們只假設他們都是有用的,但是實際上它們在這個示例中只是爲了該目的而延遲了1秒鐘

suspend fun doSomething1(): Int {
        delay(1000)
        return 13
    }

    suspend fun doSomething2(): Int {
        delay(1000)
        return 27
    }

如果需要保證這兩個函數的順序性(第二個函數依賴於第一個函數的結果),我們需要這樣寫代碼:

fun main() = runBlocking {
        val time = measureTimeMillis {
            val one = doSomething1()
            val two = doSomething2()
            println("The answer is ${one + two}")
        }
        println("Completed in $time ms")
    }
    //輸出
    The answer is 40
Completed in 2019 ms

使用async併發

如果我們想併發執行兩個suspend函數,我們可以使用async
在概念上,async 就類似於 launch。它啓動了一個單獨的協程,這是一個輕量級的線程並與其它所有的協程一起併發的工作。不同之處在於 launch 返回一個 Job 並且不附帶任何結果值,而 async 返回一個 Deferred —— 一個輕量級的非阻塞 future, 這代表了一個將會在稍後提供結果的 promise。你可以使用 .await() 在一個延期的值上得到它的最終結果, 但是 Deferred 也是一個 Job,所以如果需要的話,你可以取消它。

fun main() = runBlocking {
        val time = measureTimeMillis {
            val one = async { doSomething1() }
            val two = async { doSomething2() }
            println("The answer is ${one.await() + two.await()}")
        }
        println("Completed in $time ms")
    }
     //輸出
    The answer is 40
Completed in 1036 ms

使用協程進行併發總是顯式的。

使用惰性啓動async

使用一個可選的參數 start 並傳值 CoroutineStart.LAZY,可以對 async 進行惰性操作。 只有當結果需要被 await 或者如果一個 start 函數被調用,協程纔會被啓動。運行下面的示例:

fun main() = runBlocking {
       val time = measureTimeMillis {
            val one = async(start = CoroutineStart.LAZY) { doSomething1() }
            val two = async(start = CoroutineStart.LAZY) { doSomething2() }
            one.start()
            two.start()
            println("The answer is ${one.await() + two.await()}")
        }
        println("Completed in $time ms")
    }
    //輸出
    The answer is 40
Completed in 1036 ms

因此,在先前的例子中這裏定義的兩個協程沒有被執行,但是控制權在於程序員準確的在開始執行時調用 start。我們首先 調用 one,然後調用 two,接下來等待這個協程執行完畢。

注意,如果我們在 println 中調用了 await 並且在這個協程中省略調用了 start,接下來 await 會開始執行協程並且等待協程執行結束, 因此我們會得到順序的行爲,但這不是惰性啓動的預期用例。 當調用掛起函數計算值的時候 async(start = CoroutineStart.LAZY) 用例是標準的 lazy 函數的替換方案。

協程上下文與調度器

協程總是運行在一些以CoroutineContext類型爲代表的上下文中,它們唄定義在了Kotlin的標準庫裏。

協程向下文是各種不同元素的集合。其中主要元素是協程中的Job。

調度器與線程

協程上下文包括了一個協程調度器(CoroutineDispatcher),它確定了相應的協程在執行時使用一個或多個線程。協程調度器可以將協程的執行侷限在指定的線程中,調度它運行在線程池中或讓它不受限的運行。

所有的協程構建器諸如launch和async接受一個可選的CoroutineContext參數,它可以被用來顯示的爲一個新攜程或其他上下文元素指定一個調度器

fun main() = runBlocking {
        launch { // 運行在父協程的上下文中,即 runBlocking 主協程
            println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(Dispatchers.IO) { // 不受限的——將工作在主線程中
            println("IO            : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(Dispatchers.Unconfined) { // 不受限的——將工作在主線程中
            println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(Dispatchers.Default) { // 將會獲取默認調度器
            println("Default               : I'm working in thread ${Thread.currentThread().name}")
        }
        launch(newSingleThreadContext("MyOwnThread")) { // 將使它獲得一個新的線程
            println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
        }
    }
    //輸出

IO            : I'm working in thread DefaultDispatcher-worker-1
Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-2
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main


當調用 launch { …… } 時不傳參數,它從啓動了它的 CoroutineScope 中承襲了上下文(以及調度器)。在這個案例中,它從 main 線程中的 runBlocking 主協程承襲了上下文。

Dispatchers.Unconfined 是一個特殊的調度器且似乎也運行在 main 線程中,但實際上, 它是一種不同的機制,這會在後文中講到。

該默認調度器,當協程在 GlobalScope 中啓動的時候被使用, 它代表 Dispatchers.Default 使用了共享的後臺線程池, 所以 GlobalScope.launch { …… } 也可以使用相同的調度器—— launch(Dispatchers.Default) { …… }。

newSingleThreadContext 爲協程的運行啓動了一個新的線程。 一個專用的線程是一種非常昂貴的資源。 在真實的應用程序中兩者都必須被釋放,當不再需要的時候,使用 close 函數,或存儲在一個頂級變量中使它在整個應用程序中被重用。

非限制調度器 和 受限調度器

Dispatchers.Unconfined協程調度器在被調用的線程中啓動協程,但是這隻有直到程序運行到第一個掛起點的時候才行。掛起後,它將在完全由該所運行的線程中恢復掛起被調用的函數。非受限的調度器是合適的,當協程沒有消耗 CPU 時間或更新共享數據(比如UI界面)時它被限制在了指定的線程中。

另一方面,默認的,一個調度器承襲自外部的 CoroutineScope。 而 runBlocking 協程的默認調度器,特別是, 被限制在調用它的線程,因此承襲它在限制有可預測的 FIFO 調度的線程的執行上是非常有效果的。


fun main() = runBlocking {
        launch(Dispatchers.Unconfined) { // 非受限的——將和主線程一起工作
            println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
            delay(500)
            println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
        }
        launch { // 父協程的上下文,主 runBlocking 協程
            println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
            delay(1000)
            println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
        }
    }

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

因此,該協程從 runBlocking {……} 協程中承襲了上下文並在主線程中執行,同時使用非受限調度器的協程從被執行 delay 函數的默認執行者線程中恢復。

非受限的調度器是一種高級機制,可以在某些極端情況下提供幫助而不需要調度協程以便稍後執行或產生不希望的副作用, 因爲某些操作必須立即在協程中執行。 非受限調度器不應該被用在通常的代碼中。

在不同的線程間跳轉

fun main() = runBlocking {
        newSingleThreadContext("Ctx1").use { ctx1 ->
            newSingleThreadContext("Ctx2").use { ctx2 ->
                runBlocking(ctx1) {
                    println("Started in ctx1 :${Thread.currentThread().name}")
                    withContext(ctx2) {
                        println("Working in ctx2 :${Thread.currentThread().name}")
                    }
                    println("Back to ctx1 :${Thread.currentThread().name}")
                }
            }
        }
    }
    
    
    Started in ctx1 :Ctx1
Working in ctx2 :Ctx2
Back to ctx1 :Ctx1

它演示了一些新技術。其中一個使用 runBlocking 來顯式指定了一個上下文,並且另一個使用 withContext 函數來改變協程的上下文,而仍然駐留在相同的協程中.

注意:在這個例子中,當我們不再需要某個在 newSingleThreadContext 中創建的線程的時候, 它使用了Kotlin標準庫中的use函數來釋放該線程。

子協程

當一個協程被其它協程在 CoroutineScope 中啓動的時候, 它將通過 CoroutineScope.coroutineContext 來承襲上下文,並且這個新協程的 Job 將會成爲父協程任務的 子 任務。當一個父協程被取消的時候,所有它的子協程也會被遞歸的取消。

然而,當 GlobalScope 被用來啓動一個協程時,它與作用域無關且是獨立被啓動的。

fun main() = runBlocking {
        // 啓動一個協程來處理某種傳入請求(request)
        val request = launch {
            // 孵化了兩個子任務, 其中一個通過 GlobalScope 啓動
            GlobalScope.launch {
                println("job1: I run in GlobalScope and execute independently!")
                delay(1000)
                println("job1: I am not affected by cancellation of the request")
            }
            // 另一個則承襲了父協程的上下文
            launch {
                delay(100)
                println("job2: I am a child of the request coroutine")
                delay(1000)
                println("job2: I will not execute this line if my parent request is cancelled")
            }
        }
        delay(500)
        request.cancel() // 取消請求(request)的執行
        delay(1300) // 延遲一秒鐘來看看發生了什麼
        println("main: Who has survived request cancellation?")
    }
    
    
    
job1: I run in GlobalScope and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?

在Activity中使用協程的正確姿勢

class MainActivity<T> : Activity(), CoroutineScope {

    lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Default + job

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

    }

    fun doSomething() {
        // 在示例中啓動了10個協程,且每個都工作了不同的時長
        repeat(10) { i ->
            launch {
                delay((i + 1) * 200L) // 延遲200毫秒、400毫秒、600毫秒等等不同的時間
                println("Coroutine $i is done")
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

讓我們把有關上下文、子協程以及任務的知識梳理一下。假設我們的應用程序中有一個在生命週期中的對象,但這個對象並不是協程。假如,我們寫了一個 Android 應用程序並在上下文中啓動了多個協程來爲 Android activity 進行異步操作來拉取以及更新數據,或作動畫等。當 activity 被銷燬的時候這些協程必須被取消以防止內存泄漏。

我們通過創建一個 Job 的實例來管理協程的生命週期,並讓它與我們的 activity 的生命週期相關聯。當一個 activity 被創建的時候一個任務(job)實例被使用 Job() 工廠函數創建,並且當這個 activity 被銷燬的時候它也被取消

協程工具類

如果不想這麼強耦合的使用協程,那麼這裏也有一個小的工具類,供大家使用


internal class CoroutineLifecycleListener<T : Job>(private val deferred: T) : LifecycleObserver {

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        if (!deferred.isCancelled) {
            deferred.cancel()
        }
    }
}

fun LifecycleOwner.coroutineLifecycle(block: suspend () -> Unit): Job {
    val job = GlobalScope.launch {
        block()
    }
    lifecycle.addObserver(CoroutineLifecycleListener(job))
    return job
}

fun <T> LifecycleOwner.postDelay(delay: Long, block: suspend () -> T): Deferred<T> {
    val deferred = GlobalScope.async(Dispatchers.Main) {
        delay(delay)
        block()
    }
    lifecycle.addObserver(CoroutineLifecycleListener(deferred))
    return deferred
}


fun <T> LifecycleOwner.load(context: CoroutineContext = Dispatchers.Unconfined, loader: suspend () -> T): Deferred<T> {
    val deferred = GlobalScope.async(context, start = CoroutineStart.LAZY) {

        loader()
    }
    lifecycle.addObserver(CoroutineLifecycleListener(deferred))
    return deferred
}


infix fun <T> Deferred<T>.then(block: suspend (T) -> Unit): Job {
    return GlobalScope.launch(Dispatchers.Main) {
        try {
            block(this@then.await())
        } catch (e: Exception) {
            Log.e("Coroutine", e.toString())
            throw  e
        }
    }
}

可以看到每一個方法,大部分方法都是LifecyclerOwner的擴展類,並且監聽了Activity的生命週期,在Owner銷燬時,取消協程,防止內存泄漏,這樣在大部分場景下都可以使用了。

異常處理

這部分內容包括異常處理以及取消異常。我們已經知道當協程取消的時候會在掛起點拋出CancellationException,並且他在協程機制中被忽略了。但是如果一個異常在取消期間被拋出或多個子協程在同一個協程中拋出異常將會發生什麼?

異常的傳播

協程構建器兩種風格:自動傳播異常(launch)或者暴露給用戶(async)。前者對待異常不處理,類似Java的Thread.uncaughtExceptionHandler,後者依賴用戶來最終消耗異常。舉個例子

 val job = GlobalScope.launch {
            println("Throwing exception from launch")
            throw IndexOutOfBoundsException() // 我們將在控制檯打印 Thread.defaultUncaughtExceptionHandler
        }
//        try {
            job.join()
//        }catch (e:IndexOutOfBoundsException){}
        println("Joined failed job")
        val deferred = GlobalScope.async {
            println("Throwing exception from async")
            throw ArithmeticException() // 沒有打印任何東西,依賴用戶去調用等待
        }
        try {
            deferred.await()
            println("Unreached")
        } catch (e: ArithmeticException) {
            println("Caught ArithmeticException")
        }
        
        //輸出
        
        Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-1" java.lang.IndexOutOfBoundsException
	at com.knight.eventbus.CoroutinesTest$main$1$job$1.invokeSuspend(CoroutinesTest.kt:21)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
	at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:236)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
Joined failed job
Throwing exception from async
Caught ArithmeticException

CoroutineExceptionHandler

但是如果不想將所有的異常打印在控制檯中呢? CoroutineExceptionHandler 上下文元素被用來將通用的 catch 代碼塊用於在協程中自定義日誌記錄或異常處理。 它和使用 Thread.uncaughtExceptionHandler 很相似。

在 JVM 中可以重定義一個全局的異常處理者來將所有的協程通過 ServiceLoader 註冊到 CoroutineExceptionHandler。 全局異常處理者就如同 Thread.defaultUncaughtExceptionHandler 一樣,在沒有更多的指定的異常處理者被註冊的時候被使用。 在 Android 中, uncaughtExceptionPreHandler 被設置在全局協程異常處理者中。

CoroutineExceptionHandler 僅在預計不會由用戶處理的異常上調用, 所以在 async 構建器中註冊它沒有任何效果。

fun main() = runBlocking {
        //sampleStart
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught $exception")
        }
        val job = GlobalScope.launch(handler) {
            throw AssertionError()
        }
        val deferred = GlobalScope.async(handler) {
            throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
        }

        joinAll(job, deferred)
    }
    
    Caught java.lang.AssertionError
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章