Kotlin协程 Coroutines 使用分析

67 阅读6分钟

协程官网地址: 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

上下文 还可以 加减

image.png

Job 负责管理协程生命周期 (包名: kotlinx.coroutines)

image.png

  • 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")
        }
    }
}