我们可以看到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 {
//省略…
}
}
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
}
ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key
}
CoroutineName
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
public companion object Key : CoroutineContext.Key
}
现在要开始要集中注意力了。我们可以看到他们都是实现了Element
接口,同时都有个CoroutineContext.Key
类型的伴生对象key
,这个属性的作用是什么呢。那我们就得回过头来看看CoroutineContext
接口的几个方法了。
public operator fun
public fun 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
我们先从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.DEFAULT
D/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
和GlobalScope
2个顶级作用域。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”)
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
设计模式学习笔记
设计模式系列学习视频
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-bMD8ajin-1711948447065)]
[外链图片转存中…(img-qtqbujR9-1711948447065)]
[外链图片转存中…(img-vxr6lZsL-1711948447066)]
[外链图片转存中…(img-9arjpYvM-1711948447066)]
[外链图片转存中…(img-DmU1ZGme-1711948447066)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
设计模式学习笔记
[外链图片转存中…(img-eE1cBpfk-1711948447066)]
设计模式系列学习视频
[外链图片转存中…(img-SogSi39B-1711948447067)]