android kotlin 协程(一)
config:
-
system: macOS
-
android studio: 2022.1.1 Electric Eel
-
gradle: gradle-7.5-bin.zip
-
android build gradle: 7.1.0
-
Kotlin coroutine core: 1.6.4
前言:最近系统的学习了一遍协程, 计划通过10篇左右blog来记录一下我对协程的理解, 从最简单的 runBlocking开始; 到最后 suspend和continuation的关系等等
tips:前面几篇全都是协程的基本使用,没有源码,等后面对协程有个基本理解之后,才会简单的分析一下源码!
学习我这个系列的协程, 只需要记住一点, suspend函数 永远不会阻塞main线程执行! 永远是异步的!
看完本篇你将会学到哪些知识:
- runBlocking()
- CoroutineScope#launch()
- CoroutineScope#async()
- Job的常用方法
- 协程状态[isActive,isCancelled,isCompleted]
runBlocking
定义: runBlocking 会阻塞线程来等待自己子协程执行完, 并且对于不是子协程的作用域,也会尽量的去执行,
首先来了解一下什么是自己的子协程
通常我们通过
- CoroutineScope.launch{}
- CoroutineScope.async{}
来开启一个协程,因为当前是在CoroutineScope作用域中,所以直接launch / async 即可
这段代码可以看出,runBlocking 会等待子协程全部执行完,然后在结束任务,因为协程都是异步的,
所以会先执行协程之外的代码,然后再执行协程中的代码
可以在协程中添加一些睡眠操作再来测试一下
可以看出,还是可以正常的执行完所有代码
现在解释完了定义中的前半句话: runBlocking 会阻塞线程来等待自己子协程执行完, 并且对于不是子协程的作用域,也会尽量的去执行,
再来看一下后半句话:
可以看出,通过自定义coroutine 和 GlobalScope,来创建的协程照样可以执行出来
那么在他们之中稍微添加一点逻辑会怎么样?
可以看出,一旦添加了一点逻辑, runBlocking是不会等待非子协程的作用域
如果想让runBlocking等待非子协程执行完,那么只需要调用Job.#join() 即可
例如这样:
join()方法目前可以理解为: 等待当前协程执行完 在往下执行其他代码,
一旦调用了join()方法,那么协程就变成了同步的,那么这块代码一共执行需要4s
因为协程1并没有join, 所以协程1还是异步的,
协程2调用了join,所以在执行协程2的过程中,协程1也在执行.
所以协程1,与协程2的执行时间为2s
tips: 在开发中不建议使用runBlocking,因为会阻塞主线程,阻塞主线程的时间,用来子协程的执行..
开启协程两种不同的方式
在上面代码中我们提到了,开启协程有2种方式
- CoroutineScope#launch{}
- CoroutineScope#async{}
先来看相同点:
相同点就是无论是哪种方式,都会执行里面的代码
那么这两种方式有什么区别呢?
-
launch无法返回数据, async可以返回结果
返回的结果通过 Deferred#await()
来获取,并且调用Deferred#await()
的时候,会等待async{} 执行完成之后在往下执行,就和Job#join
一样,不过await()
有返回结果
使用await的时候有一个注意点:
那么也可以看到,launch{} 与 async{} 的返回值也有所不同:
- launch{} 返回 Job
- async{} 返回Deferred
其实本质上来说,async 返回的也是Job,不过只是Job的子类Deferred而已,Deferred只是对返回值等一些操作的封装
那么Job是用来干什么的呢?
Job是用来管理协程的生命周期的, 例如刚才提到的 Job.join() 就可以让协程 “立即执行”
launch{} 和 async{} 捕获异常的方式也不同,这个等下一篇专门聊异常的时候在详细讲解
Job.cancel 取消协程
协程比线程好用的一点就是协程可以自己管理生命周期, 而线程则不可以
这里需要注意的是,如果协程体中还在执行,但是外部已经取消了,那么则会throw异常出来
JobCancellationException
fun main() = runBlocking<Unit> {
println("main start")
val job = launch {
try {
(0..100).forEachIndexed { index, _ ->
delay(1000)
println("launch $index")
}
} catch (e: Exception) {
println("协程被取消了 $e")
}
}
// 协程执行完成监听
job.invokeOnCompletion {
println("协程执行完毕${it}")
}
delay(5500)
// 取消协程
job.cancel()
println("main end")
}
Job#invokeOnCompletion: 协程完成回调
运行结果:
main start
launch 0
launch 1
launch 2
launch 3
launch 4
main end
协程被取消了 kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@76a4d6c
协程执行完毕kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelled}@76a4d6c
coroutine的3种状态
coroutine的三种状态都是通过Job来管理的:
- isActive 是否是活跃状态
- isCancelled 是否取消
- isCompleted 是否完成
先来看看正常流程执行的代码:
我们知道协程始终是异步执行的,在执行printlnJob的时候,协程体中的代码还没有真正的执行
所以此时处于活跃状态,并且协程没有被执行完
如果我们在协程执行完成的回调中调用
那么此时,协程体中的代码已经执行完了,那么此时就是非活跃状态
还剩一个Job#isCancelled 这个方法比较简单,简单的说就是是否调用了Job.cancel()
但是这里有一个特别奇怪的点,明明已经调用Job#cancel() 来取消协程,并且协程体中的代码也没执行,但是为什么还显示协程没有执行完呢?
因为Job#cancel() 并不是suspend函数,不是suspend函数就没有恢复功能,这行文字可能看的有一点迷惑,先不用管什么挂起于恢复,现在只需要知道
我们调用cancel() 的时候会紧跟着一个,Job#join() 即可
或者直接调用Job.cancelAndJoin() 即可
挂起恢复,这4个字我理解了10天左右,不可能通过本篇就讲清楚,现在只需要会调用这些api,即可!!
那么问题就来了,这个状态有什么用呢?
先来看一段代码:
可以惊奇的发现,这段代码无论如何都cancel不掉.好像是失效了一样
那么解决这个问题,就可以检测协程是否是活跃状态,例如这样
Job也提供了一个方法: Job#ensureActive()
ensureActive() 本质也是通过isActive判断,不同的是,当取消的时候可以捕获到取消的异常,然后来处理对应的事件
图片地址: gitee.com/lanyangyang…
回顾一下本篇:
本篇我们讲解了runBlocking, 这个函数会帮我们阻塞主线程, 阻塞住线程的时候会等待内部的子协程全部执行完
还聊了最基础的如何开启一个协程, launch / async 以及他们的相同点和不同点
最后引出了协程生命周期管理者Job, 讲解了Job常用的方法,以及job的3种状态
方法名 | 作用 | 补充 |
---|---|---|
join() | 立即恢复协程体执行 | 等待协程体执行完成,在执行后续代码 |
cancel() | 取消协程 | ,如果取消时,协程体还在执行,这throw JobCancellationException,这个异常不会上报,会自行处理 |
invokeOnCompletion() | 协程体执行完成回调 | |
isActive | 协程体是否是活跃状态 | |
isCancelled | 协程体是否被取消 | |
isCompleted | 协程体是否执行完成 |
下一篇预告:
-
CoroutineDispatcher // 协程调度器 用来切换线程
-
CoroutineName // 协程名字
-
CoroutineStart // 协程启动模式
-
CoroutineException // launch / async 捕获异常
-
GlobalCoroutineException // 全局捕获异常
原创不易,您的点赞就是我最大的支持!