-
本文概览:
- 什么是协程上下文,有什么用,是如何构成的,如何解构;协程中的作用域的分类,有什么细节;协程的启动模式是什么,有哪几种?协程中的 Job是什么,其与 Deferred接口的区别?
协程上下文:
-
是什么:Kotlin协程中的一个基本构成单位,包含一些用户定义的数据
-
有什么用:利用上下文实现协程对线程行为、生命周期、异常、调试等一系列操作
-
CoroutineContext: 协程上下文
-
线程行为、生命周期、异常以及调试:
-
包含用户定义的一些数据集合,这些数据与协程密切相关
-
它是一个有索引的 Element 实例集合,一个介于 set 和 map之间的数据结构。每个 element 在这个集合有一个唯一的 Key
- 拿到可key就能拿到具体元素
-
-
协程上下文的构成:
-
Job(属于上下文的一种): 控制协程的生命周期
-
CoroutineDispatcher: 向合适的线程分发任务
- 协程在哪里执行
- 协程看似随机分配在线程上,但实际上还是
- 类似于RxJava的schedule,控制RxJava到底在那一个线程
-
CoroutineName: 协程的名称,调试的时候很有用
- 协程的名字是一个哈希码,用这个可以对其重命名
-
CoroutineExceptionHandler: 处理未被捕捉的异常
- Java中也有全局捕获
-
协程上下文测试
-
概述:
- 可以打印协程上下文各构成部分,并且支持加减操作
-
代码:PC平台
fun main(){ val t = CorountineTest2() t.start() Thread.sleep(1000) } class CorountineTest2 { fun coroutineContextTest(){ val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext") //get操作符重载去获取协程上下文内的东西 log("$coroutineContext,${coroutineContext[CoroutineName]}") //minusKey:可以动态调整协程上下文的打印结果 val newCoroutineContext = coroutineContext.minusKey(CoroutineName) log("$newCoroutineContext") } } -
运行结果:
-
在android平台(模拟器)上运行:添加代码
//在按钮的点击事件中添加 val t = CorountineTest2() t.coroutineContextTest() -
运行结果:模拟器与PC平台一致
源码分析:coroutineContext
-
概述:
- coroutineContext为一个接口,内部提供了get()方法
-
整体结构:
-
重要方法:get
-
get:
- 可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问
public operator fun <E: element> get(key:Key<E>): E?
-
plus:
- 和 Set.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。+ 运算符可以很容易的用于结合上下文,但是有一个很重要的事情需要小心 —— 要注意它们结合的次序,因为这个 + 运算符是不对称的。
public operator fun plus(context: CoroutineContext): CoroutineContext{...} -
fold:
- 和 Collection.fold 扩展函数类似,提供遍历当前 context 中所有 Element 的能力
public fun <R> fold(initial: R, operation: (R, Element) -> R): R -
minusKey:
- 返回一个上下文,其中包含该上下文中的元素,但不包含具有指定key的元素。
public fun minusKey(key: Key<*>): CoroutineContext
-
-
重要接口:
-
key:
public interface Key<E : Element> -
Element:继承自CoroutineContext,通过key找到element
public interface Element : CoroutineContext {...}
-
Kotlin 中的协程上下文解构效果
-
加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext
-
代码:
package com.zero.jiangke import kotlinx.coroutines.* fun main(){ val t = CorountineTest2() t.coroutineContextTest1() Thread.sleep(1000) } class CorountineTest2 { fun coroutineContextTest1(){ val coroutineContext = Dispatchers.Default + CoroutineName("myContext") log("$coroutineContext,${coroutineContext[CoroutineName]}") val newCoroutineContext = coroutineContext + Dispatchers.IO //所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext log("$newCoroutineContext") } } -
运行结果:
-
整体继承关系:
Kotlin 协程中的 Job
-
Job 负责管理协程的生命周期:属于element,实际上也是一个上下文
-
不同 Job的实现具有一定的层级关系
-
父job可以包含子job
-
-
协程之间是有生命周期转换的
-
细节:
- 会等待子 job执行结束,父 job才结束执行
- 发生异常时进入cancelling状态,接着进入cancelled状态
-
示意图:从说明文档中来的
-
-
Job中的关键方法:
-
start:启动协程
public fun start(): Boolean- 执行逻辑:调用该函数来启动这个 Coroutine,如果当前 Coroutine 还没有执行调用该函数返回 true,如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
-
cancel:取消协程
public fun cancel(): Unit = cancel(null)- 执行逻辑:通过可选的取消原因取消此作业。 原因可以用于指定错误消息或提供有关取消原因的其他详细信息,以进行调试。
- 注意cancel函数有多种实现
-
invokeOnCompletion:为 job 设置通知
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle-
执行逻辑:通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个Handler,通过Handler拿到返回的通知。
-
执行状态:CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:
- 如果 Job 是正常执行完成的,则 cause 参数为 null
- 如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
- 其他情况表示 Job 执行失败了。
-
这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 Job 的完成情况了, 则可以调用 DisposableHandle.dispose 函数来取消监听。如果 Job 已经执行完了, 则无需调用 dispose 函数了,会自动取消监听
-
-
join函数:
public suspend fun join()-
特别说明:由suspend 修饰,只能在其他suspend函数或者协程作用域中调用执行
-
执行逻辑:
- 这个函数会暂停当前所处的 Coroutine直到该Coroutine执行完成。所以 Job 函数一般用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。
- 当 Job 执行完成后, job.join 函数恢复,这个时候 job 这个任务已经处于完成状态了,而调用 job.join 的Coroutine还继续处于 activie 状态。
- 请注意,只有在其所有子级都完成后,作业才能完成
-
该函数的挂起是可以被取消的,并且始终检查调用的Coroutine的Job是否取消。如果在调用此挂起函数或将其挂起时,调用Coroutine的Job被取消或完成,则此函数将引发 CancellationException
-
-
Kotlin协程中的Deferred接口
-
整体示意:
-
重要方法:await()
public suspend fun await(): T-
执行逻辑:类似于Furture
- 用来等待这个Coroutine执行完毕并返回结果
-
Kotlin 中的suspend关键字
- 工作机制:被修饰的函数若为耗时操作,遇到挂起点时将其修饰的函数则会压入一个栈,主线程继续执行;并不会阻塞当前线程;当耗时操作返回完,再恢复到挂起点进行执行;
Kotlin 中的CoroutineDispatcher
-
总结:
- 由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher
-
四种方式:
-
Dispatchers.Default:内部是Java线程池
- 默认的调度器,适合处理后台计算,是一个CPU密集型任务调度器。如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。
-
Dispatchers.IO:RxJava中也有一个这样的I/O
- 顾名思义这是用来执行阻塞 IO 操作的,是和Default共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。
-
Dispatchers.Unconfined:
- 由于Dispatchers.Unconfined未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume的线程决定恢复协程的线程。
-
Dispatchers.Main:
- 指定执行的线程是主线程,在Android上就是UI线程·
-
Kotlin 中的协程的启动模式:CoroutineStart
-
四种启动模式:
-
CoroutineStart.DEFAULT:
- 协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态 虽然是立即调度,但也有可能在执行前被取消
-
CoroutineStart.ATOMIC:
- 协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消;虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行
-
CoroutineStart.LAZY:
- 只要协程被需要时,包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态
-
CoroutineStart.UNDISPATCHED:
- 协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点;是立即执行,因此协程一定会执行
-
-
使用细节:
-
对于launch:第一个参数就是设置协程的启动模式的
-
代码:
//点击事件添加代码: val t = CoroutineTest2() t.start() fun start(){ log("start ....") val launchJob = GlobalScope.launch(context = Dispatchers.Main,start = CoroutineStart.LAZY) { Thread.sleep(10000)//可能导致ANR log("launch 启动一个协程") } Thread.sleep(2000) launchJob.start() log("launchJob= $launchJob") } -
运行截图:launch默认是会新开一个线程执行协程,但可以通过指定协程上下文,指定协程附着在UI线程上(这个时候,以launch方式启动的协程就会阻塞主线程了)
-
协程只是说:异步代码同步化,将任务进一步分割了,至于说会不会阻塞UI线程,要看你是怎么玩的
-
Kotlin 中协程作用域
-
包含协程上下文:控制协程生命周期、启动模式、异常处理;
-
每一个协程创建是在协程作用域上的:CoroutineScope 只是定义了一个新 Coroutine 的执行 Scope。每个 coroutine builder 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 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) } }
-
-