注:编码工具为IntelliJ
目录
概念及作用
协程上下文是一个有索引的Element实例集合,每个element在这个集合里有一个唯一的key;
协程上下文包含用户定义的一些数据集合,这些数据与协程密切相关;
协程上下文用于控制线程行为、协程的生命周期、异常以及调试。
组成结构
协程上下文结构图如下:
Job:用于控制协程的声明周期。
CoroutineDispatcher:用于向合适的线程分发任务。
CoroutineName:协程名称,可以用于调试。
CoroutineExceptionHandler:用于处理未被捕获的异常。
public interface CoroutineContext {
// 可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样
// 使用context[key] 这种中括号的形式来访问
public operator fun <E : Element> get(key: Key<E>): E?
// 和Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
// 和 Set.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有
// Element,如果遇到重复的(Key 一样的),
// 那么用+号右边的 Element 替代左边的。+ 运算符可以很容易的用于结合上下文,但是有一个很重要的
// 事情需要小心 —— 要注意它们结合的次序,因为这个 + 运算符是不对称的。
public operator fun plus(context: CoroutineContext): CoroutineContext{...}
// 返回一个上下文,其中包含该上下文中的元素,但不包含具有指定key的元素。
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {...}
}
Job
用于管理协程的生命周期。生命周期状态切换示意图如下:
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
Job的各个方法的作用:
public interface Job : CoroutineContext.Element {
// 调用该函数来启动这个 Coroutine,如果当前 Coroutine 还没有执行调用该函数返回 true,如果当前
// Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
public fun start(): Boolean
// 通过可选的取消原因取消此作业。 原因可以用于指定错误消息或提供有关取消原因的其他详细信息,以
// 进行调试。
public fun cancel(): Unit = cancel(null)
// 通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个通知函数。
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
// 其中CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:
// – 如果 Job 是正常执行完成的,则 cause 参数为 null
// – 如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该
// 当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
// – 其他情况表示 Job 执行失败了。
// 这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 Job 的完成情况了, 则可以调用
// DisposableHandle.dispose 函数来取消监听。如果 Job 已经执行完了, 则无需调用 dispose 函
// 数了,会自动取消监听
// join 函数和前面三个函数不同,这是一个 suspend 函数。所以只能在 Coroutine 内调用这个函数
// 会暂停当前所处的 Coroutine直到该Coroutine执行完成。所以 Job 函数一般用来在另外一个
// Coroutine 中等待 job 执行完成后继续执行。
// 当 Job 执行完成后, job.join 函数恢复,这个时候 job 这个任务已经处于完成状态了,而调用
// job.join 的Coroutine还继续处于 activie 状态。
// 请注意,只有在其所有子级都完成后,作业才能完成
// 该函数的挂起是可以被取消的,并且始终检查调用的Coroutine的Job是否取消。如果在调用此挂起函数
// 或将其挂起时,调用Coroutine的Job被取消或完成,则此函数将引发 CancellationException
public suspend fun join()
}
Defered
Defered继承自Job,功能与Job相同,区别是,Defered有一个await方法。
public suspend fun await(): T -》 Future
// 用来等待这个Coroutine执行完毕并返回结果。
CoroutineDispatcher
协程分发器,用于分发任务到合适的线程,类似于RxJava的Schedulers。有四种可选的调度器,如下所述:
CoroutineDispatcher:
- Dispatchers.Default
默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器。如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。
- Dispatchers.IO
顾名思义这是用来执行阻塞 IO 操作的,是和Default共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。
- Dispatchers.Unconfined
由于Dispatchers.Unconfined未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume的线程决定恢复协程的线程。
- Dispatchers.Main:
指定执行的线程是主线程,在Android上就是UI线程·
由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher
CoroutineStart
协程启动模式,类似于Android中Activity的启动模式。也有四种可选项,如下所述:
CoroutineStart:
- CoroutineStart.DEFAULT:
协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态
虽然是立即调度,但也有可能在执行前被取消
- CoroutineStart.ATOMIC:
协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消
虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行
- CoroutineStart.LAZY:
只要协程被需要时,包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态
- CoroutineStart.UNDISPATCHED:
协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点
是立即执行,因此协程一定会执行
CoroutineScope
协程作用域,定义了协程的作用范围,内部持有一个协程上下文对象。
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
协程作用域分类
// CoroutineScope 只是定义了一个新 Coroutine 的执行 Scope。每个 coroutine builder 都是
// CroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext分类及行为规则
// 官方框架在实现复合协程的过程中也提供了作用域,主要用以明确写成之间的父子关系,以及对于取消或
// 者异常处理等方面的传播行为。该作用域包括以下三种:
// - 顶级作用域
// 没有父协程的协程所在的作用域为顶级作用域。
// - 协同作用域
// 协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用
// 域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
// 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)
}
}
协程上下文的使用
案例一:自定义协程上下文配置
package step_thirteen
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import step_twelve.log
fun main() {
var context = Job() + Dispatchers.Default + CoroutineName("customContext")
log("context = $context")
log("context[CoroutineName] = ${context[CoroutineName]}")
context = context.minusKey(CoroutineName)
log("context = $context")
}
输出:
[2021-11-25 11:39:03]-[main] context = [JobImpl{Active}@76fb509a, CoroutineName(customContext), Dispatchers.Default]
[2021-11-25 11:39:03]-[main] context[CoroutineName] = CoroutineName(customContext)
[2021-11-25 11:39:03]-[main] context = [JobImpl{Active}@76fb509a, Dispatchers.Default]
由于CoroutineContext中对+运算符进行了重载,所以上面代码中可以使用+运算符定义协程上下文对象。
案例二:分发器实践,向主线程分发任务
在Android开发中尤其需要注意,由于协程可以嵌套,当最外层指定了主线程分发器,则内部所有代码可能都在主线程中执行,此时不宜做耗时操作,否则可能引发问题。一下代码在Android系统上运行。
GlobalScope.launch(context = Dispatchers.Main) {
log("运行在主线程中的launch方式启动的协程")
}
输出:
2021-11-25 12:14:38.773 26916-26916/com.wcc.kt E/MainActivity: [2021-11-25 04:14:38]-[main] 运行在主线程中的launch方式启动的协程
案例三:启动模式实践
package step_thirteen
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import step_twelve.log
fun main() {
val coroutine = GlobalScope.launch(start = CoroutineStart.LAZY){
log("调用start才会打印这句")
}
coroutine.start()
// launch启动的协程是非阻塞的,所以需要主线程等待一会
// Android中不需要,因为主线程一直阻塞,不会执行完毕
Thread.sleep(100)
}
输出:
[2021-11-25 11:56:42]-[DefaultDispatcher-worker-1] 调用start才会打印这句