面试官vo,史上最详Android版kotlin协程入门进阶实战(二),从入门到精通系列Android高级工程师路线介绍
我这里整理了一份完整的学习思维以及Android开发知识大全PDF。当然实践出真知,即使有了学习线路也要注重实践,学习过的内容只有结合实操才算是真正的掌握。本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
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处恢复执行时,因为所在的协程有调度器,所以后面的执行将会在调度器对应的线程上执行。
同样的我们点到为止,关于启动模式的的相关内容我们就现讲到这里。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。






既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
最后
我这里整理了一份完整的学习思维以及Android开发知识大全PDF。

当然实践出真知,即使有了学习线路也要注重实践,学习过的内容只有结合实操才算是真正的掌握。
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
8)]
当然实践出真知,即使有了学习线路也要注重实践,学习过的内容只有结合实操才算是真正的掌握。
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
更多推荐



所有评论(0)