由于文章涉及到的只是点比较多、内容可能过长,可以根据自己的能力水平和熟悉程度分阶段跳着看。如有讲述的不正确的地方劳烦各位私信给笔者,万分感谢。
kotlin协程的关键知识点
上一本章节
《Android kotlin协程入门实战(一):kotlin协程的基础用法解读》末尾我们提到,将在本章节中对以下知识点做初步讲解,包含上文提到的
launch和
async函数中的3个参数作用。清单如下:
CoroutineDispatcherCoroutineContext作用CoroutineStartCoroutineScopesuspend关键字的作用当然还有一些其他的知识点也是很重要的,比如:
CoroutineExceptionHandler、
Continuation、
Scheduler、
ContinuationInterceptor等。但是确实涉及到的东西比较多,如果都展开的话,可能再写几个篇幅都没有办法讲完。上面这些是笔者认为掌握了这些知识点以后,基本可以开始着手项目实战了。我们后面在实战的过程中,边写边讲解。
上文我们提到一个协程调度器
CoroutineDispatcher的概念,调度器又是一个什么神奇的东西。在这里我们对调度器不做过多深入的解释,这可是协程的三大件之一,后面我们会有专门的篇幅做深入讲解。为了方便我们把协程调度器简称为
调度器,那接下来我们就看看什么是调度器。偷个懒,引用一下官方的原话:
对于调度器的实现机制我们已经非常清楚了,官方框架中预置了4个调度器,我们可以通过
Dispatchers对象直接访问它们:
public actual object Dispatchers { @JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher() @JvmStatic
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher @JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined @JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
复制代码
Default:默认调度器,CPU密集型任务调度器,适合处理后台计算。通常处理一些单纯的计算任务,或者执行时间较短任务。比如:Json的解析,数据计算等IO:IO调度器,,IO密集型任务调度器,适合执行IO相关操作。比如:网络处理,数据库操作,文件操作等Main:UI调度器, 即在主线程上执行,通常用于UI交互,刷新等Unconfined:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。比如上面我们通过
launch启动的时候,因为我们没有传入参数,所有实际上它使用的是默认调度器
Dispatchers.Default
GlobalScope.launch{
Log.d("launch", "启动一个协程")
}//等同于GlobalScope.launch(Dispatchers.Default){
Log.d("launch", "启动一个协程")
}
复制代码
Dispatchers.IO和
Dispatchers.Main就都很好理解了。这是我们以后在Android开发过程中,打交道最多的2个调度器。比如后台数据上传,我们就可以使用
Dispatchers.IO调度器。刷新界面我们就使用
Dispatchers.Main调度器。为方便使用官方在Android协程框架库中,已经为我们定义好了几个供我们开发使用,如:
MainScope、
lifecycleScope、
viewModelScope。它们都是使用的
Dispatchers.Main,这些后续我们都将会使用到。
根据我们上面使用的方法,我们好像只有在启动协程的时候,才能指定具体使用那个
Dispatchers调度器。如果我要是想中途切换线程怎么办,比如:
IO线程上,而刷新UI是在
主线程上,那我们应该怎么办。莫慌,莫慌,万事万物总有解决的办法。官方为我们提供了一个
withContext顶级函数,使用
withContext函数来改变协程的上下文,而仍然驻留在相同的协程中,同时
withContext还携带有一个泛型
T返回值。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
//......
}
复制代码
呀,这一看
withContext这个东西好像很符合我们的需求嘛,我们可以先使用
launch(Dispatchers.Main)启动协程,然后再通过
withContext(Dispatchers.IO)调度到
IO线程上去做网络请求,把得到的结果返回,这样我们就解决了我们上面的问题了。
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) { //网络请求...
"请求结果"
}
btn.text = result
}
复制代码
是不是很简单!!! 麻麻再也不会说我的handler满飞了,也不用走那万恶的回调地狱了。我想怎么切就怎么切,想去走个线程就去哪个线程。逻辑都按着顺序一步一步走,而且代码都是这么的丝滑。还要什么自行车,额.错了,还要什么handler,管他回调不回调。
CoroutineContext即协程上下文。它是一个包含了用户定义的一些各种不同元素的
Element对象集合。其中主要元素是
Job、协程调度器
CoroutineDispatcher、还有包含协程异常
CoroutineExceptionHandler、拦截器
ContinuationInterceptor、协程名
CoroutineName等。这些数据都是和协程密切相关的,每一个
Element都一个唯一key。
public interface CoroutineContext { public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E? public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R public operator fun plus(context: CoroutineContext): CoroutineContext = if (context === EmptyCoroutineContext) this else context.fold(this) { ...} public fun minusKey(key: Key<*>): CoroutineContext //注意这里,这个key很关键
public interface Key <E : CoroutineContext.Element> public interface Element : CoroutineContext { public val key: Key<*> public override operator fun <E : Element> get(key: Key<E>): E? = if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this) public override fun minusKey(key: Key<*>): CoroutineContext = if (this.key == key) EmptyCoroutineContext else this
}
}
复制代码
我们可以看到
Element是
CoroutineContext的内部接口,同时它又实现了
CoroutineContext接口,这么设计的原因是为了保证
Element中一定只能存放的
Element它自己,而不能存放其他类型的数据
CoroutineContext内还有一个内部接口
Key,同时它又是
Element的一个属性,这个属性很重要,我们先在这里插个眼,待会再讲解这个属性的作用。
那我们上面提到
Job、
CoroutineDispatcher、
CoroutineExceptionHandler、
ContinuationInterceptor、
CoroutineName等为什么又可以存放到
CoroutineContext中呢。我们接着往下看看它们各自的实现:
Job
public interface Job : CoroutineContext.Element { public companion object Key : CoroutineContext.Key<Job> { //省略...
}
}
复制代码
CoroutineDispatcher
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
}
复制代码
CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element { public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
}
复制代码
ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
复制代码
CoroutineName
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) { public companion object Key : CoroutineContext.Key<CoroutineName>
}
复制代码
现在要开始要集中注意力了。我们可以看到他们都是实现了
Element接口,同时都有个
CoroutineContext.Key类型的伴生对象
key,这个属性的作用是什么呢。那我们就得回过头来看看
CoroutineContext接口的几个方法了。
public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): Rpublic operator fun plus(context: CoroutineContext): CoroutineContext = if (context === EmptyCoroutineContext) this else context.fold(this) { ...}public fun minusKey(key: Key<*>): CoroutineContext
复制代码
我们先从
plus方法说起,
plus有个关键字
operator表示这是一个运算符重载的方法,类似List.plus的运算符,可以通过
+号来返回一个包含原始集合和第二个操作数中的元素的结果。同理
CoroutineContext中是通过
plus来返回一个由原始的
Element集合和通过
+号引入的
Element产生新的
Element集合。
get方法,顾名思义。可以通过
key 来获取一个
Element
fold方法它和集合中的fold是一样的,用来遍历当前协程上下文中的
Element集合。
minusKey方法
plus作用相反,它相当于是做减法,是用来取出除
key以外的当前协程上下文其他
Element,返回的就是不包含
key的协程上下文。
现在我们就知道为什么我们之前说
Element中的
key这个属性很重要了吧。因为我们就是通过它从协程上下文中获取我们想要的
Element,同时也解释为什么
Job、
CoroutineDispatcher、
CoroutineExceptionHandler、
ContinuationInterceptor、
CoroutineName等等,这些
Element都有需要有一个
CoroutineContext.Key类型的伴生对象
key。我们写个测试方法: 如:
private fun testCoroutineContext(){
val coroutineContext1 = Job() + CoroutineName("这是第一个上下文")
Log.d("coroutineContext1", "$coroutineContext1")
val coroutineContext2 = coroutineContext1 + Dispatchers.Default + CoroutineName("这是第二个上下文")
Log.d("coroutineContext2", "$coroutineContext2")
val coroutineContext3 = coroutineContext2 + Dispatchers.Main + CoroutineName("这是第三个上下文")
Log.d("coroutineContext3", "$coroutineContext3")
}
复制代码
D/coroutineContext1: [JobImpl{Active}@21a6a21, CoroutineName(这是第一个上下文)]
D/coroutineContext2: [JobImpl{Active}@21a6a21, CoroutineName(这是第二个上下文), Dispatchers.Default]
D/coroutineContext3: [JobImpl{Active}@21a6a21, CoroutineName(这是第三个上下文), Dispatchers.Main]
复制代码
我们通过对比日志输出信息可以看到,通过
+号我们可以把多个
Element整合到一个集合中,同时我们也发现:
Job是同一个对象。CoroutineName,新增的
CoroutineName替换了第一个上下文中的
CoroutineName。CoroutineName和
Dispatchers,同时他们也替换了第二个上下文中的
CoroutineName和
Dispatchers。但是因为这个
+运算符是不对称的,所以在我们实际的运用过程中,通过
+增加
Element的时候一定要注意它们结合的顺序。那么现在关于协程上下文的内容就讲到这里,我们点到为止,后面在深入理解阶段在细讲这些东西运行的原理细节。
CoroutineStart协程启动模式,是启动协程时需要传入的第二个参数。协程启动有4种:
DEFAULT 默认启动模式,我们可以称之为饿汉启动模式,因为协程创建后立即开始调度,虽然是立即调度,单不是立即执行,有可能在执行前被取消。
LAZY 懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度。也就是说只有我们主动的调用
Job的
start、
join或者
await等函数时才会开始调度。
ATOMIC 一样也是在协程创建后立即开始调度,但是它和
DEFAULT模式有一点不一样,通过
ATOMIC模式启动的协程执行到第一个挂起点之前是不响应
cancel 取消操作的,
ATOMIC一定要涉及到协程挂起后
cancel 取消操作的时候才有意义。
UNDISPATCHED 协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。这听起来有点像
ATOMIC,不同之处在于
UNDISPATCHED是不经过任何调度器就开始执行的。当然遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器。
我们可以通过一个小例子的来看看这几个启动模式的实际情况:
private fun testCoroutineStart(){
val defaultJob = GlobalScope.launch{
Log.d("defaultJob", "CoroutineStart.DEFAULT")
}
defaultJob.cancel()
val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
Log.d("lazyJob", "CoroutineStart.LAZY")
}
val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC){
Log.d("atomicJob", "CoroutineStart.ATOMIC挂起前")
delay(100)
Log.d("atomicJob", "CoroutineStart.ATOMIC挂起后")
}
atomicJob.cancel()
val undispatchedJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){
Log.d("undispatchedJob", "CoroutineStart.UNDISPATCHED挂起前")
delay(100)
Log.d("atomicJob", "CoroutineStart.UNDISPATCHED挂起后")
}
undispatchedJob.cancel()
}
复制代码
每个模式我们分别启动一个一次,
DEFAULT模式启动时,我们接着调用了
cancel取消协程,
ATOMIC模式启动时,我们在里面增加了一个挂起点
delay挂起函数,来区分
ATOMIC启动时的挂起前后执行情况,同样的
UNDISPATCHED模式启动时,我们也调用了
cancel取消协程,我们看实际的日志输出情况:
D/defaultJob: CoroutineStart.DEFAULTD/atomicJob: CoroutineStart.ATOMIC挂起前 D/undispatchedJob: CoroutineStart.UNDISPATCHED挂起前 复制代码
或者
D/undispatchedJob: CoroutineStart.UNDISPATCHED挂起前 D/atomicJob: CoroutineStart.ATOMIC挂起前 复制代码
为什么会出现2种情况。我们上面提到过
DEFAULT模式协程创建后立即开始调度,但不是立即执行,所有它有可能会被
cancel取消,导致没有输出
defaultJob这条日志。
同样的
ATOMIC模式启动的时候也接着调用了
cancel取消协程,但是因为没有遇到挂起点,所以挂起前的日志输出了,但是挂起后的日志没有输出。
而
UNDISPATCHED模式启动的时候也接着调用了
cancel取消协程,同样的因为没有遇到挂起点所以输出了
UNDISPATCHED挂起前,但是因为
UNDISPATCHED是立即执行的,所以他的日志
UNDISPATCHED挂起前输出在
ATOMIC挂起前的前面。
接着我们在补充一下关于
UNDISPATCHED模式。我们上面有提到当以
UNDISPATCHED模式启动时,遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器。这句话我们又要怎么理解呢。我们还是以一个例子来认识解释
UNDISPATCHED模式,比如:
private fun testUnDispatched(){
GlobalScope.launch(Dispatchers.Main){
val job = launch(Dispatchers.IO) {
Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
delay(100)
Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
}
Log.d("${Thread.currentThread().name}线程", "-> join前")
job.join()
Log.d("${Thread.currentThread().name}线程", "-> join后")
}
}
复制代码
那我们将会看到如下输出,挂起前后都在一个
worker-1线程里面执行:
D/main线程: -> join前 D/DefaultDispatcher-worker-1线程: -> 挂起前 D/DefaultDispatcher-worker-1线程: -> 挂起后 D/main线程: -> join后 复制代码
现在我们在稍作修改,我们在子协程
launch的时候使用
UNDISPATCHED模式启动:
private fun testUnDispatched(){
GlobalScope.launch(Dispatchers.Main){
val job = launch(Dispatchers.IO,start = CoroutineStart.UNDISPATCHED) {
Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
delay(100)
Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
}
Log.d("${Thread.currentThread().name}线程", "-> join前")
job.join()
Log.d("${Thread.currentThread().name}线程", "-> join后")
}
}
复制代码
那我们将会看到如下输出:
D/main线程: -> 挂起前 D/main线程: -> join前 D/DefaultDispatcher-worker-1线程: -> 挂起后 D/main线程: -> join后 复制代码
我们看到当以
UNDISPATCHED模式即使我们指定了协程调度器
Dispatchers.IO,
挂起前还是在
main线程里执行,但是
挂起后是在
worker-1线程里面执行,这是因为当以
UNDISPATCHED启动时,协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点。遇到挂起点之后的执行,将取决于挂起点本身的逻辑和协程上下文中的调度器,即
join处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。
我们再改一下,把子协程在
launch的时候使用
UNDISPATCHED模式启动,去掉
Dispatchers.IO调度器,那又会出现什么情况呢
private fun testUnDispatched(){
GlobalScope.launch(Dispatchers.Main){
val job = launch(start = CoroutineStart.UNDISPATCHED) {
Log.d("${Thread.currentThread().name}线程", "-> 挂起前")
delay(100)
Log.d("${Thread.currentThread().name}线程", "-> 挂起后")
}
Log.d("${Thread.currentThread().name}线程", "-> join前")
job.join()
Log.d("${Thread.currentThread().name}线程", "-> join后")
}
}
复制代码
D/main线程: -> 挂起前 D/main线程: -> join前 D/main线程: -> 挂起后 D/main线程: -> join后 复制代码
我们发现它们都在一个线程里面执行了。这是因为当通过
UNDISPATCHED启动后遇到挂起,
join处恢复执行时,如果所在的协程没有指定调度器,那么就会在
join处恢复执行的线程里执行,即
挂起后是在父协程
(Dispatchers.Main线程里面执行,而最后
join后这条日志的输出调度取决于这个最外层的协程的调度规则。
现在我们可以总结一下,当以
UNDISPATCHED启动时:
无论我们是否指定协程调度器,
挂起前的执行都是在当前线程下执行。
如果所在的协程没有指定调度器,那么就会在
join处恢复执行的线程里执行,即我们上述案例中的
挂起后的执行是在
main线程中执行。
当我们指定了协程调度器时,遇到挂起点之后的执行将取决于挂起点本身的逻辑和协程上下文中的调度器。即
join处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。
同样的我们点到为止,关于启动模式的的相关内容我们就现讲到这里。
协程作用域
CoroutineScope为协程定义作用范围,每个协程生成器
launch、
async等都是
CoroutineScope的扩展,并继承了它的
coroutineContext自动传播其所有
Element和取消。协程作用域本质是一个接口,不建议手工实现该接口,而应该首选委托实现。下面我们列出了部分
CoroutineScope相关定义:
public interface CoroutineScope { public val coroutineContext: CoroutineContext
}public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
ContextScope(coroutineContext + context)public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext
}public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
复制代码
我们可以看到
CoroutineScope也重载了
plus方法,通过
+号来新增或者修改我们
CoroutineContext协程上下文中的
Element。同时官方也为我们定义好了
MainScope和
GlobalScope2个顶级作用域。
GlobalScope我们已经很熟了,前面的案例都是通过它来实现的。
MainScope我们可以看到它的上下文是通过
SupervisorJob和
Dispatchers.Main组合的,说明它是一个在主线程执行的协程作用域,我们在后续的Android实战开发中,会结合Activity、Fragment,dialog等使用它。这里不再继续往下扩展。
至于
SupervisorJob分析它之前,我们得先说一下协程作用域的分类。我们之前提到过父协程和子协程的概念,既然有父协程和子协程,那么必然也有父协程作用域和子父协程作用域。不过我们不是这么称呼,因为他们不仅仅是父与子的概念。协程作用域分为三种:
顶级作用域 --> 没有父协程的协程所在的作用域称之为顶级作用域。
协同作用域 --> 在协程中启动一个协程,新协程为所在协程的子协程。子协程所在的作用域默认为协同作用域。此时子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。
主从作用域 官方称之为
监督作用域。与协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。但是如果父协程被取消,则所有子协程同时也会被取消。
同时补充一点:父协程需要等待所有的子协程执行完毕之后才会进入
Completed状态,不管父协程自身的协程体是否已经执行完成。我们在最开始提到协程生命周期的时候就提到过下,现在回过头看是不是感觉很流程变得清晰。
wait children +-----+ start +--------+ complete +-------------+ finish +-----------+| New | -----> | Active | ---------> | Completing | -------> | Completed |+-----+ +--------+ +-------------+ +-----------+ | cancel / fail | | +----------------+ | | V V +------------+ finish +-----------+ | Cancelling | --------------------------------> | Cancelled | +------------+ +-----------+ 复制代码
子协程会继承父协程的协程上下文中的
Element,如果自身有相同key的成员,则覆盖对应的
key,覆盖的效果仅限自身范围内有效。这个就可以用上我们前面学到的协程上下文
CoroutineContext的知识,小案例奉上:
private fun testCoroutineScope(){
GlobalScope.launch(Dispatchers.Main){
Log.d("父协程上下文", "$coroutineContext")
launch(CoroutineName("第一个子协程")) {
Log.d("第一个子协程上下文", "$coroutineContext")
}
launch(Dispatchers.Unconfined) {
Log.d("第二个子协程协程上下文", "$coroutineContext")
}
}
}
复制代码
日志顺序的问题我们前面已经分析过原因,如果还不懂的话,麻烦您回到基础用法里面仔细的再看一遍。
D/父协程上下文: [StandaloneCoroutine{Active}@81b6e46, Dispatchers.Main]
D/第二个子协程协程上下文: [StandaloneCoroutine{Active}@f6b7807, Dispatchers.Unconfined]
D/第一个子协程上下文: [CoroutineName(第一个子协程), StandaloneCoroutine{Active}@bbe6d34, Dispatchers.Main]
复制代码
可以看到第一个子协程的覆盖了父协程的
Job,但是它继承了父协程的调度器
Dispatchers.Main,同时也新增了一个
CoroutineName。第二个子协程覆盖了父协程的
Job,也将父协程的调度器覆盖为
Unconfined,但是他没有继承第一个子协程的
CoroutineName,这就是我们说的覆盖的效果仅限自身范围内有效。接下来我们看看上面提到的
协同作用域和
主从(监督)作用域异常传递和协程取消的问题。
我们上面提到
协同作用域如果子协程抛出未捕获的异常时,会将异常传递给父协程处理,如果父协程被取消,则所有子协程同时也会被取消。先上代码看看效果:
private fun testCoroutineScope2() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
Log.d("scope", "--------- 1")
launch(CoroutineName("scope2") + exceptionHandler) {
Log.d("scope", "--------- 2") throw NullPointerException("空指针")
Log.d("scope", "--------- 3")
}
val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
Log.d("scope", "--------- 4")
delay(2000)
Log.d("scope", "--------- 5")
}
scope3.join()
Log.d("scope", "--------- 6")
}
}
复制代码
D/scope: --------- 1 D/scope: --------- 2 D/exceptionHandler: CoroutineName(scope1) java.lang.NullPointerException: 空指针 复制代码
可以看到子协程
scope2抛出了一个异常,将异常传递给父协程
scope1处理,但是因为任何一个子协程异常退出会导致整体都将退出。所以导致父协程
scope1未执行完成成就被取消,同时还未执行完子协程
scope3也被取消了。
主从(监督)作用域与
协同作用域一致,区别在于该作用域下的协程取消操作的单向传播性,子协程的异常不会导致其它子协程取消。分析
主从(监督)作用域的时候,我们需要用到
supervisorScope或者
SupervisorJob,如下代码块:
private fun testCoroutineScope3() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
supervisorScope {
Log.d("scope", "--------- 1")
launch(CoroutineName("scope2")) {
Log.d("scope", "--------- 2") throw NullPointerException("空指针")
Log.d("scope", "--------- 3")
val scope3 = launch(CoroutineName("scope3")) {
Log.d("scope", "--------- 4")
delay(2000)
Log.d("scope", "--------- 5")
}
scope3.join()
}
val scope4 = launch(CoroutineName("scope4")) {
Log.d("scope", "--------- 6")
delay(2000)
Log.d("scope", "--------- 7")
}
scope4.join()
Log.d("scope", "--------- 8")
}
}
}
复制代码
D/scope: --------- 1 D/scope: --------- 2 D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指针 D/scope: --------- 6 D/scope: --------- 7 D/scope: --------- 8 复制代码
可以看到子协程
scope2抛出了一个异常,并将异常传递给父协程
scope1处理,同时也结束了自己本身。因为在于
主从(监督)作用域下的协程取消操作是单向传播性,因此协程
scope2的异常并没有导致父协程退出,所以
6
7
8都照常输出,而
3
4
5因为在协程
scope2里面所以没有输出。
我们刚刚使用了
supervisorScope实现了
主从(监督)作用域,那我们通过
SupervisorJob又该如何实现呢。我们把
supervisorScope称之为
主从(监督)作用域,那么
SupervisorJob就可以称之为
主从(监督)作业,如下:
private fun testCoroutineScope4() {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
}
val coroutineScope = CoroutineScope(SupervisorJob() +CoroutineName("coroutineScope"))
GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
with(coroutineScope){
val scope2 = launch(CoroutineName("scope2") + exceptionHandler) {
Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")
throw NullPointerException("空指针")
}
val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
scope2.join()
Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")
delay(2000)
Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")
}
scope2.join()
Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")
coroutineScope.cancel()
scope3.join()
Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")
}
Log.d("scope", "6--------- ${coroutineContext[CoroutineName]}")
}
}
复制代码
D/scope: 1--------- CoroutineName(scope2) D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指针 D/scope: 2--------- CoroutineName(scope3) D/scope: 4--------- CoroutineName(coroutineScope) D/scope: 5--------- CoroutineName(coroutineScope) D/scope: 6--------- CoroutineName(scope1) 复制代码
是不是感觉和
supervisorScope的用法很像,我们通过创建了一个
SupervisorJob的主从(监督)协程作用域,调用了子协程的
join是为了保证它一定是会执行。同样的子协程
scope2抛出了一个异常,通过协程
scope2自己内部消化了,同时也结束了自己本身。
因为协程
scope2的异常并没有导致
coroutineScope作用域下的协程取消退出,所以协程
scope3照常运行输出
2,后又因为调用了我们定义的协程作用域
coroutineScope的
cancel方法取消了协程,所以即使我们后面调用了协程
scope3的
join,也没有输出
3,因为
SupervisorJob的取消是向下传播的,所以后面的
4
5都是在
coroutineScope的作用域中输出的。
现在我们关于协程作用域
CoroutineScope的作用我们已经有了一个大概的了解,同样的因为这个篇幅中我们是基础讲解,所以我们点到为止,如果还想深入了解,那就只能看后面的深入协程篇幅。
通过前面的篇幅我们已经知道,使用
suspend关键字修饰的函数叫作
挂起函数,
挂起函数只能在协程体内,或着在其他
挂起函数内调用。那挂起又是啥玩意呢。
我估计各位看到这里的时候,可能有些人已经被上面的知识点弄的有点晕乎,别急,先放松下大脑,喝杯水,然后做个眼保健操缓解一下。下面开始敲黑板了,打起精神,要开始划重点了。
首先一个
挂起函数既然要挂起,那么他必定得有一个
挂起点,不然我们怎么知道函数是否挂起,从哪挂起呢。 我们定义一个空实现的
suspend方法,然后通过AS的工具栏中
Tools->
kotlin->
show kotlin ByteCode解析成字节码
private suspend fun test(){
}
复制代码
final synthetic test(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 复制代码
public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>)}
复制代码
我们看到
test方法需要的是一个
Continuation接口,官方给的介绍是用于挂起点之后,返回类型为
T的值用的。那我们又是怎么拿到的这个
Continuation呢。要解开这个问题我们得先回到协程的创建和运行是的过程。
我们启动一个协程无非是通过
launch,
async等方法。我们之前有说到过他们的启动模式
CoroutineStart,但是并没有深入的去分析它的创建和启动过程,我们这里先回过头大概的看一下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block) return coroutine
}
复制代码
我们看到在通过
launch启动一个协程的时候,他通过
coroutine的
start方法启动协程,然后我们接着往下看
public fun start(start: CoroutineStart, block: suspend () -> T) {
initParentJob()
start(block, this)
}public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
initParentJob()
start(block, receiver, this)
}
复制代码
然后
start方法里面调用了
CoroutineStart的
invoke,这个时候我们发现了
Continuation。
public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(completion)
ATOMIC -> block.startCoroutine(completion)
UNDISPATCHED -> block.startCoroutineUndispatched(completion)
LAZY -> Unit // will start lazily
}public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily
}
复制代码
而
Continuation又是通过
start方法传进来的
coroutine。所以现在可以确定,我们的协程体本身就是一个
Continuation,这也就解释了为什么可以在协程体内调用
suspend挂起函数了。
现在我们也可以确定,在协程内部
挂起函数的调用处就是
挂起点,如果
挂起点出现异步调用,那么当前协程就被挂起,直到对应的
Continuation通过调用
resumeWith函数才会恢复协程的执行,同时返回
Result<T>类型的成功或者失败的结果。
由于章节主题的限制,这里我们就不再下深入了。需要注意的是
挂起函数不一定真的会挂起,如果只是提供了挂起的条件,但是协程没有产生异步调用,那么协程还是不会被挂起。
预告:下一篇我们将会讲解kotlin协程中的异常处理,其实我们在这篇章节中已经,提到了一些异常处理,没有注意的同学可以回到
协程作用域看看。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69983917/viewspace-2795354/ ,如需转载,请注明出处,否则将追究法律责任。