2024年Android最新史上最详Android版kotlin协程入门进阶实战(二),2024年最新2024腾讯Android面试题精选

最后

代码真的是重质不重量,质量高的代码,是当前代码界提倡的,当然写出高质量的代码肯定需要一个相当高的专业素养,这需要在日常的代码书写中逐渐去吸收掌握,谁不是每天都在学习呀,目的还不是为了一个,为实现某个功能写出高质量的代码。

所以,长征路还长,大家还是好好地做个务实的程序员吧。

最后,小编这里有一系列Android提升学习资料,有兴趣的小伙伴们可以来看下哦~

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • Unconfined:非受限调度器,又或者称为“无所谓”调度器,不要求协程执行在特定线程上。

比如上面我们通过launch启动的时候,因为我们没有传入参数,所有实际上它使用的是默认调度器Dispatchers.Default

GlobalScope.launch{

Log.d(“launch”, “启动一个协程”)

}

//等同于

GlobalScope.launch(Dispatchers.Default){

Log.d(“launch”, “启动一个协程”)

}

Dispatchers.IODispatchers.Main就都很好理解了。这是我们以后在Android开发过程中,打交道最多的2个调度器。比如后台数据上传,我们就可以使用Dispatchers.IO调度器。刷新界面我们就使用Dispatchers.Main调度器。为方便使用官方在Android协程框架库中,已经为我们定义好了几个供我们开发使用,如:MainScopelifecycleScopeviewModelScope。它们都是使用的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

}

}

我们可以看到ElementCoroutineContext的内部接口,同时它又实现了CoroutineContext接口,这么设计的原因是为了保证Element中一定只能存放的Element它自己,而不能存放其他类型的数据CoroutineContext内还有一个内部接口Key,同时它又是Element的一个属性,这个属性很重要,我们先在这里插个眼,待会再讲解这个属性的作用。

那我们上面提到JobCoroutineDispatcherCoroutineExceptionHandlerContinuationInterceptorCoroutineName等为什么又可以存放到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,同时也解释为什么JobCoroutineDispatcherCoroutineExceptionHandlerContinuationInterceptorCoroutineName等等,这些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

  • 第三个上下文在第二个的基础上又增加了一个新的CoroutineNameDispatchers,同时他们也替换了第二个上下文中的CoroutineNameDispatchers

但是因为这个+运算符是不对称的,所以在我们实际的运用过程中,通过+增加Element的时候一定要注意它们结合的顺序。那么现在关于协程上下文的内容就讲到这里,我们点到为止,后面在深入理解阶段在细讲这些东西运行的原理细节。

协程启动模式


CoroutineStart协程启动模式,是启动协程时需要传入的第二个参数。协程启动有4种:

  • DEFAULT 默认启动模式,我们可以称之为饿汉启动模式,因为协程创建后立即开始调度,虽然是立即调度,单不是立即执行,有可能在执行前被取消。

  • LAZY 懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度。也就是说只有我们主动的调用Jobstartjoin或者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为协程定义作用范围,每个协程生成器launchasync等都是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。同时官方也为我们定义好了 MainScopeGlobalScope2个顶级作用域。GlobalScope我们已经很熟了,前面的案例都是通过它来实现的。

MainScope我们可以看到它的上下文是通过SupervisorJobDispatchers.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 大厂面试真题解析

资料展示:

image

image

image

image

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值