协程官网地址: developer.android.google.cn/kotlin/coro…
理解协程用法:将异步逻辑同步化
基本使用
//创建一个新协程并阻塞调用他的线程,直到里面的代码块执行完毕,返回值是泛型T
runBlocking {
log("协程基本使用")
}
//调用后 runBlocking 源码: 返回 coroutine.joinBlocking() 阻塞
//创建一个新协程但不会阻塞调用线程,条件-》必须要在协程作用域(CoroutineScope)中才能调用,返回值Job(可以用于取消协程)
GlobalScope.launch {
log("launch 协程基本使用")
}
//创建一个新协程但不会阻塞调用线程,返回值Deferred。(Deferred 也是继承Job, 只是比Job多了一个await(), await()可以获取返回的值)
GlobalScope.async {
log("async 协程基本使用")
"TestAsync"
}
- runBlocking 主要用于测试(main里面)
- GlobalScope 虽然是基本使用,但是不建议这样用(需要使用CoroutineScope来创建自定义启动)。这样启动协程存在;当界面销毁后协程还存在的情况,会消耗资源,因此不推荐这样方式启动,尤其是频繁启动协程
- launch 和 async 的区别是: async 有一个返回值, 而launch无
自定义协程启动
根据 GlobalScope 可以写出:
//CoroutineScope 是一个接口, 通过访问 CoroutineScope 函数 创建
val customScope = CoroutineScope(EmptyCoroutineContext)
class CustomScope: CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
协程参数概念
CoroutineContext 协程上下文
1.线程线程行为、生命周期、异常以及调试
2.包含用户自定义一些数据集合,这些数据与协程密切相关
3.是一个有索引Element实例集合,介于set和map之间的数据结构。每个Element有一个唯一的Key
- Job 控制协程生命周期
- CoroutineDispatcher 协程分发任务
- CoroutineName 协程名称(对于调试有用)
- CoroutineExceptionHandler 处理未捕获的异常
// CoroutineContext -> 定义一个协程上下文
val contextCoroutine = Job() + CoroutineName("Name") + Dispatchers.Default
上下文 还可以 加减
Job 负责管理协程生命周期 (包名: kotlinx.coroutines)
- start() 函数 -> 调用该函数启动Coroutine, 如果当前Coroutine 还未执行该函数当调用后返回true, 如果当前 Coroutine 已经执行或者已经执行完毕, 则返回false
- cancel() 函数 -> 通过此函数取消作业。 可用于指定错误信息或者提供有关取消原因的其他信息,方便调试。
- invokeOnCompletion()函数 -> 通过此函数设置Job 完成通知, 当Job执行完成时会同步执行此函数, 并且此函数包含三个状态:
1.job 正常执行完成, Cause 则返回 null
2.job 正常取消, 则 Cause 为 CancellationException ,当这种情况时不能当成异常处理
3.其他情况表示取消失败
只有子级完成后, 作业才算完成
Deferred -> await()函数,等待Coroutine执行并返回结果
suspend 挂起函数标志
CoroutineDispatcher 调度器
- Dispatchers.Default 默认调度器,适合CPU密集任务调度
- Dispatchers.Main 主线程(android上也就是UI线程)
- Dispatchers.Unconfined 未定义线程池
- Dispatchers.IO 执行阻塞IO操作, 和 Default共用一个共享线程池里面执行任务, 根据同时运行的任务数量,在需要的时候创建额外的线程, 当任务执行完毕后会释放不需要的线程
总结: 子Coroutine 会继承父 coroutine 的 Content, 所以为了方便使用, 一般会在父 Coroutine 上设定一个Dispatcher, 然后所有子 Coroutine 自动使用这个Dispatcher
CoroutineStart 协程启动模式
DEFAULT //默认自己启动调度, 其将直接进入取消响应状态。虽然是立即调度,但是也有可能执行前被取消
LAZY //设置这个启动模式需要手动启动
ATOMIC //创建后立即开始调度, 协程执行到第一个挂起点之前不响应取消。 虽然是立即调度,但是将调度和执行合二为一,,保证调度和执行是原子性操作,因此协程也一定会执行
UNDISPATCHED // 协程创建后立即执行当前函数调用栈中执行, 直到遇到第一个真正挂起的点。 是立即执行,因此一定会执行
CoroutineScope 协程作用域
- 顶级作用域
没有父协程的协程所在的作用域为顶级作用域
- 协同作用域
协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出异常的未捕获异常, 都将传递给父协程处理,父协程同时也会被取消
// 协同列子
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch {
//调用的
coroutineScope {
}
}
// 库 定义的
// coroutineScope 内部异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常都会推出,会导致整体的退出
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
- 主从作用域
与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程
//supervisorScope 属于主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
协程在Android使用
- 正确的使用是按照如下
//SupervisorJob -> 满足主从作用域
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch {
// 不会开启新的协程, 在指定协程上挂起代码块,并挂起协程直到代码块运行结束
withContext(Dispatchers.IO) {
}
}
scope.launch {
//创建新协程
launch {
}
}
//取消的话调用
scope.cancel()
lifecycleScope.launch {
}
// 生命周期使用
lifecycleScope.launch {
whenCreated { }
whenResumed { }
whenStarted { }
}
协程异常捕获
fun test1() {
val handler = CoroutineExceptionHandler {_, e ->
log("协程 抛出异常 ${e.stackTraceToString()}")
}
// 返回 Job, 才能捕获, 而且不能在子协程中捕获
GlobalScope.launch(context = handler) {
readFile()
}
}
fun readFile() {
val file = FileInputStream(File(""))
file.read()
}
主从作用域例子:
fun test2() {
GlobalScope.launch {
// 主从作用域 例子
val job = SupervisorJob()
with(CoroutineScope(coroutineContext + job)) {
val jobNum = launch {
for (i in 0..100) {
log(" i = $i")
}
}
val jobRead = launch {
readFile()
}
}
}
}
总结: 1.Job 协程需要在根协程进行捕获 2.Deferred 协程 如果需要消费返回值,则需要在消费处处理,如果不消费,则不需要处理(协程自己会处理, 也不会导致APP退出)
suspend
- 调用不会阻塞调用线程(可以看编译后的kotlin代码)
- 如果单纯的给函数加上 suspend 关键字并不会神奇的让函数变成非阻塞的
suspend fun main() {
val testB = TestCoroutinesB()
log("Main 方法开始")
testB.test()
log("Main 方法结束")
}
class TestCoroutinesB {
suspend fun test() {
log("测试suspend 关键字 1")
// 耗时操作
val num = BigInteger.probablePrime(4000, Random())
log("测试suspend 关键字 2 内容 num = $num")
}
suspend fun test1() {
withContext(Dispatchers.IO) {
log("测试suspend 关键字 1")
// 耗时操作
val num = BigInteger.probablePrime(4000, Random())
log("测试suspend 关键字 2 内容 num = $num")
}
}
}
// 当这样使用,才不会阻塞
suspend fun test1() {
GlobalScope.launch {//不会导致切换线程
withContext(Dispatchers.IO) {
log("测试suspend 关键字 1")
// 耗时操作
val num = BigInteger.probablePrime(4000, Random())
log("测试suspend 关键字 2 内容 num = $num")
}
}
}