手摸手带你走进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

你可能感兴趣的:(Android)