最后
代码真的是重质不重量,质量高的代码,是当前代码界提倡的,当然写出高质量的代码肯定需要一个相当高的专业素养,这需要在日常的代码书写中逐渐去吸收掌握,谁不是每天都在学习呀,目的还不是为了一个,为实现某个功能写出高质量的代码。
所以,长征路还长,大家还是好好地做个务实的程序员吧。
最后,小编这里有一系列Android提升学习资料,有兴趣的小伙伴们可以来看下哦~
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
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 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
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
//注意这里,这个key很关键
public interface Key
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun get(key: Key): E? =
if (this.key == key) this as E else null
public override fun 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 {
//省略…
}
}
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”)
}
最后
文章所有资料全部已经打包整理好,另外小编手头上整理了大量Android架构师全套学习资料,Android核心高级技术PDF文档+全套高级学习资料+视频+2021 BAT 大厂面试真题解析
资料展示:
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
程的协程上下文中的Element
,如果自身有相同key的成员,则覆盖对应的key
,覆盖的效果仅限自身范围内有效。这个就可以用上我们前面学到的协程上下文CoroutineContext
的知识,小案例奉上:
private fun testCoroutineScope(){
GlobalScope.launch(Dispatchers.Main){
Log.d(“父协程上下文”, “$coroutineContext”)
launch(CoroutineName(“第一个子协程”)) {
Log.d(“第一个子协程上下文”, “$coroutineContext”)
}
最后
文章所有资料全部已经打包整理好,另外小编手头上整理了大量Android架构师全套学习资料,Android核心高级技术PDF文档+全套高级学习资料+视频+2021 BAT 大厂面试真题解析
资料展示:
[外链图片转存中…(img-XFjQxtuz-1715674100510)]
[外链图片转存中…(img-nhnFCrJE-1715674100511)]
[外链图片转存中…(img-XJk4wn8a-1715674100511)]
[外链图片转存中…(img-PZdpIcgk-1715674100511)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!