Kotlin 学习笔记(五)—— 协程的基础知识,面试官的最爱了~(下)

简介: Kotlin 学习笔记(五)—— 协程的基础知识,面试官的最爱了~(下)

3.2 协程调度器


在 3.1 中已经出现过调度器的身影,就是当需要指定协程运行的线程时,使用调度器调度即可。在实际的使用中是通过 Dispatchers 对象来访问它们。官方框架预置了4个调度器:

  1. Default:默认调度器,适合处理 CPU 密集型任务,比如涉及大量计算;
  2. IO:IO 调度器,适用于执行 IO 相关操作,处理 IO 密集型任务,比如读取文件、访问数据库;
  3. Main:UI 调度器,根据平台不同会初始化为对应的 UI 线程调度器,即通常在主线程上执行的任务,比如在 Android 上就是各种更新 UI 的操作;
  4. Unconfined:没有约束的调度器,即不会要求协程在哪个线程上执行。当挂起函数结束后程序恢复运行时,这时执行协程的线程就是执行挂起函数的线程。即挂起函数由哪个线程执行,后续协程就在哪个线程执行。

Dispatchers 调度器的基类是 CoroutineDispatchers,后者是所有协程调度器的基类。而 CoroutineDispatchers 继承自 AbstractCoroutineContextElement 并实现了 ContinuationInterceptor 接口;ContinuationInterceptor 又实现了 CoroutineContext.Element 接口,最后,CoroutineContext.Element 接口实现了CoroutineContext。

兜兜转转,原来 CoroutineDispatchers 本身也是一个 CoroutineContext,这也是 code 2 中可以直接与 coroutineExceptionHandler 直接相加的原因:GlobalScope.launch (Dispatchers.Main + coroutineExceptionHandler) ,两者都是 CoroutineContext.Element 的实例当然可以相加了。

使用起来比较简单,常见的设置调度器的方法有两种:launch 方法设置、withContext 方法设置。如下 code 4 所示,在 Android 的 onCreate 方法中调用 launch 方法,并设置在 Main 线程中执行;然后通过 withContext 方法切换到 IO 线程:

// code 4    调度器指定协程运行的线程
GlobalScope.launch(Dispatchers.Main) {
     Log.d(TAG, "onCreate: ++++ Dispatchers.Main Thread is ${Thread.currentThread().name}")
     withContext(Dispatchers.IO) {
          Log.d(TAG, "onCreate: ++++ Dispatchers.IO Thread is ${Thread.currentThread().name}")
     }
     Log.d(TAG, "onCreate: ++++ Thread here is ${Thread.currentThread().name}")
}

这里用到 GlobalScope 只是为了方便,不推荐在实际开发中使用,输出的结果比较有意思:

image.png

注意到,在 withContext 切换到 IO 线程,执行完 IO 线程的逻辑后,居然自己又回到原来的 Main 线程了!太懂事了吧!这也是为什么我们可以在协程中用写同步代码的思想,去写异步的逻辑。比如我把 code 4 中在 IO 线程的操作换成网络请求数据的逻辑,然后把最后的打印的逻辑换成更新 UI 的代码,不就可以实现请求数据更新 UI 的逻辑了吗?再也不用像 RxJava 那样来回切线程了。


3.3 协程启动构建器


再看看 launch 函数的第二个参数—— CoroutineStart,协程的启动模式设置器。在说之前需要弄清 立即调度立即执行的区别。

立即调度:指的是协程的调度器会立刻接收到调度指令,但具体什么时候调度线程执行,还需要根据调度器的具体情况而定,即立即调度立即执行之间通常会有时间间隔

再来看下不同的启动模式,有四种:

  1. DEFAULT:默认值,表示协程创建后,立即开始调度,在执行前如果被取消则直接进入取消响应状态;
  2. LAZY:表示该协程只有主动调用了协程的 start 或 join 或 await 方法后才会开始调度,在执行前如果被取消则将直接进入异常结束状态;
  3. ATOMIC:表示该协程创建后,立即开始调度,且调度和执行合二为一,是原子操作,协程一定会执行,不会被取消掉,只能忽略协程的执行结果;
  4. UNDISPATCHED:表示协程创建后立即在当前函数调用栈中执行,是运行在协程创建时所在的线程。虽然与 ATOMIC 模式一样可保证协程一定执行,但 ATOMIC 会调度到指定调度器所在的线程上执行。

实际开发中,通常使用 DEFAULT 和 LAZY 这两种启动模式就够了。


3.4 协程作用域


launch 函数的第三个参数是一个由外层 CoroutineScope 调用的 lambda 闭包,我们需要在协程中处理的逻辑都在这个闭包中实现。code 2 中的GlobalScope就是一个 CoroutineScope  对象,这个对象代表将要启动的协程的作用域范围,或者说 CoroutineScope 是协程用于管理自身生命周期的对象。

常见的有 GlobalScope 和 MainScope 两种。在 Android 里还有 lifecycleScope、viewModelScope,如果要用的话分别需要在 gradle 中添加如下的库:

// code 4
// 使用 lifecycleScope 需要引用的库
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
// 使用 viewModelScope 需要引用的库
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'

当然,不同的 Scope 有不同的特性。GlobalScope:通常被用于启动一个顶级协程(顶级协程是顶级作用域,即没有父协程的作用域),这种协程的生命周期是会伴随应用的整个生命周期,不会被取消掉,所以要非常谨慎的使用,容易造成内存泄漏。可以用于数据打点,log 日志记录等,更像是一个守护线程。这个 Scope 是属于标准协程库中的。

MainScope:主要用于在 UI 主线程中运行的协程,这一点可以查看它的源码得知:

// code 5
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Dispatchers.Main 表示最后分发到 Main 主线程上了,包括 Android 在内的许多场景,主线程就代表是 UI 线程。使用 MainScope 需要注意,在当前 UI 页面将要被回收时,需要调用 cancel 方法取消,避免内存泄漏。


3.5 Job 对象


别忘了 launch 函数还有个 Job 类型的返回值,Job 对象是个接口,也是继承自 CoroutineContext.Element 。返回的这个 job 实例可以代表这个协程本身。我们拿到协程的 Job 对象之后,可以获取到协程的状态,用于表明协程状态的 3 个标记位如下:isActive:true 表示 job 为活跃的状态,已经启动并没有完成,也没有被取消;此外,父 job 在等待子 job 完成时也是处于活跃状态;isCompleted:true 表示 job 因为某种原因已经完成,值得注意的是,如果 job 被取消或者执行失败,也是已经完成状态;父 job 只有当所有子job 都是完成状态时,它才是完成状态;isCancelled:true 表示 job 因为某种原因被取消,例如 job 显式地调用 cancel 方法或者执行失败,或者它的子/父 job 被取消。

父子 job 也会相互影响自身的状态。比如,一旦父 job 被取消,其所有子 job 也会被取消;当一个子 job 由于出现异常导致执行失败,其父 job 和其他的子 job 也会立即被取消并抛出 CancellationException。这些“连带影响”可以通过  SupervisorJob 去自定义。

Job 的状态以及 3 个标记位的对应值如下表所示:

Job 状态 isActive isCompleted isCancelled
New(可选的初始态) false false false
Active(默认初始态) true false false
Completing(瞬时状态) true false false
Cancelling(瞬时状态) false false true
Cancelled(最终态) false true true
Completed(最终态) false true false


通常默认情况下,是用 CoroutineStart.DEFAULT 来启动一个协程,这时协程被创建后直接启动,进入 Active 状态;而使用 CoroutineStart.LAZY 创建后的协程则是 New 状态,直到调用 start 或 join 方法后才会进入 Active 状态。

Completing 状态属于 job 内部的状态,对于一个外部观察者来说,一个 Completing 态的 job 仍然处于 Active 态,这个 job 在内部正在等待其子 job 执行完。官方注释有个状态流转图,如下所示:

image.png

Job 接口的主要方法有如下几个:

  1. public fun start(): Boolean:启动协程,返回 true 表示启动协程成功;返回 false 表示协程已经被启动或已经执行完成。
  2. public fun cancel(cause: CancellationException? = null):取消协程,可选参数用于描述取消协程的理由或错误信息。
  3. public suspend fun join():挂起这个协程直到它完成,如果 job 处于 New 状态,此方法也可启动协程;此方法可被取消;当调用此方法的协程被取消或已完成,此方法会抛出 CancellationException。
  4. public suspend fun Job.cancelAndJoin():取消协程并挂起它,直到完成取消协程这个操作。
  5. public fun attachChild(child: ChildJob): ChildHandle:给当前协程添加一个子协程,返回的 ChildHandle 用于 detach 父协程。
  6. public fun invokeOnCompletion(handler: CompletionHandler):DisposableHandle:这个方法用于监听 job 完成或者取消的回调。如果 job 被取消,则会抛出被取消的异常。如果正常完成,则抛出 null。如下代码 code 5,如果 cancel 方法被调用,则会打印出:MainActivity: ++++++ invokeOnCompletion kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@302ae10如果不调用 cancel,则打印为 null. invokeOnCompletion 方法返回的 DisposableHandle 对象就是用于回收资源的,如果需要,调用它的 dispose 方法即可。
// code 5
val job = GlobalScope.launch(Dispatchers.Main) {
    for (i in 1..4) {
        delay(1000)
        if (i == 2) {
            cancel()    // 取消
        }
        Toast.makeText(this@MainActivity, "修之竹~", Toast.LENGTH_SHORT).show()
    }
}
job.invokeOnCompletion { throwable ->
    Log.d(TAG, "++++++ invokeOnCompletion ${throwable.toString()}")
}

此外,job 接口中所有的方法都是线程安全的。至此,协程的几个重要组成部分就介绍完了,接下来回过头来看看启动协程的常用的几个方法。


4. 协程启动常见的几种方法


启动协程主要的三种方法:

runBlocking: T:用于执行协程任务,通常只用于启动最外层的协程。常用于线程启动或切换到协程的场景

launch: Job:也是用于执行协程任务,会返回一个 Job 对象。

async/await: Deferred:同样用于执行协程任务,成对出现,await 可以得到 async 异步操作后得到的执行结果

launch 方法之前已经介绍的再清楚不过了,这里看看另外的两种。

runBlocking: T:启动一个最外层的协程,即顶级协程,没有父协程。它启动的协程是阻塞的,执行完之后才能继续往下执行,这是它的特点,从它的方法名也可以看出来。而 launch 则是非阻塞的,先来看一下非阻塞的情况:

// code 6  非阻塞协程
GlobalScope.launch {
    delay(5000)
    Log.d(TAG, " 1)launch Test: +++++ ${Thread.currentThread().name}")
}
Log.d(TAG, "2)After launch Test: +++++ ${Thread.currentThread().name}")

delay 函数也是一个挂起函数,它可以非阻塞性的挂起当前线程,并且在设置的时间间隔之后恢复执行,是可被取消的。这里就是挂起 5s 后再执行打印,下图是输出情况,注意看打印的时间:

image.png

在遇到 delay 后,下面的代码是可以继续执行的,没有被阻塞;当 delay 时间到了,再才会执行第一个打印的代码。如果换成 runBlocking 就不一样了:

// code 7 阻塞协程
runBlocking {
    delay(5000)
    Log.d(TAG, " 1)runBlocking Test: +++++ ${Thread.currentThread().name}")
}
Log.d(TAG, "2)After runBlocking Test: +++++ ${Thread.currentThread().name}")

image.png

执行到 delay 后,会将当前线程阻塞,直到时间到了才能继续往下执行。

async/await: Deferred:一般用于 async 内执行请求数据的耗时任务;await 取出 async 返回结果的场景。async 返回的是一个 Deferred 接口对象,继承自 Job,且包含一个返回结果。Deferred 是一个非阻塞的,可被取消的对象。await 是 Deferred 中的方法,可获取返回的结果数据。多个 async 还可以并行处理逻辑,举个栗子:

// code 8 多个 async 并行处理逻辑
GlobalScope.launch(Dispatchers.Main) {
    val time1 = System.currentTimeMillis()
    val task1 = async(Dispatchers.IO) {
        delay(2000)
        Log.d("TAG", "task1 Current Thread:${Thread.currentThread().name}")
        "task1 返回值"
    }
    val task2 = async(Dispatchers.IO) {
        delay(1000)
        Log.d("TAG", "task2 Current Thread:${Thread.currentThread().name}")
        "task2 返回值"
    }
    Log.d("TAG", "task1 返回值:${task1.await()}  task2 返回值:${task2.await()}")
    Log.d("TAG", "task1 和 task2 总耗时:${System.currentTimeMillis() - time1}")
}

image.png

从打印结果可以看出,delay 时间短的 task2 率先完成,总时长为 2s,说明 task1 和 task2 两个任务并行处理了。但是,如果两个 async 方法后面紧接着处理各自的 await 方法,则就是串行处理了,看下面的效果:

// code 9 多个 async 串行处理
GlobalScope.launch(Dispatchers.Main) {
    val time1 = System.currentTimeMillis()
    val task1 = async(Dispatchers.IO) {
        delay(2000)
        Log.d("TAG", "task1 Current Thread:${Thread.currentThread().name}")
        "task1 返回值"
    }.await()
    val task2 = async(Dispatchers.IO) {
        delay(1000)
        Log.d("TAG", "task2 Current Thread:${Thread.currentThread().name}")
        "task2 返回值"
    }.await()
    Log.d("TAG", "task1 和 task2 总耗时:${System.currentTimeMillis() - time1}")
}

image.png

看打印结果,task1 执行完才会执行 task2,总耗时是两个任务耗时的总和。Why? 这是因为 await 函数也是一个挂起函数,协程执行到 await 时会被挂起,当 async 执行完返回结果后,才会继续执行。而在 code 8 中两个 await 函数都是在两个 async 之后,所以在两个 async 中的任务就是并行处理的关系了。

协程说到这里,基本的概念应该差不多了,本篇笔记主要归纳总结了协程的主要组成部分以及它们的基本使用,概念的东西比较多,也是重中之重。但协程的知识远不止这些,希望此篇能起到抛砖引玉的作用,希望大家能在项目中使用协程,你会发现,用了就真的回不去了~


参考文献


  1. 极客时间 Kotlin 系列课程;  张涛
  2. 《Kotlin 核心编程》; 霍丙乾 水滴技术团队
  3. Android 上的 Kotlin 协程  官方文档  developer.android.google.cn/kotlin/coro…
  4. Kotlin:lifecycleScope与GlobalScope以及MainScope的区别,详细分析为什么在Android中推荐使用lifecycleScope!  ;pumpkin的玄学  blog.csdn.net/weixin_4423…
  5. Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的  rengwuxian.com/kotlin-coro…  ;LewisLuo(罗宇)
  6. Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了  rengwuxian.com/kotlin-coro…  ;Hugo(谢晨成)
  7. Kotlin 协程:简单理解 runBlocking, launch ,withContext ,async,doAsync blog.csdn.net/Jason_Lee15…           ;Jason_Lee155

欢迎关注我的个人公众号:修之竹

赞人玫瑰,手留余香!一个点赞、一次转发都是对我最好的鼓励~

目录
相关文章
|
18天前
|
Java 开发者 Kotlin
Kotlin学习笔记- 类与构造器
本篇笔记详细介绍了Kotlin中的类与构造器,包括类的基本概念、主构造器与次构造器的区别、构造器中参数的使用规则、类的继承以及构造器在继承中的应用等。通过具体示例,解释了如何在类中定义属性、实现构造逻辑,并探讨了Kotlin类的继承机制和Any类的作用。此外,还简要介绍了包的概念及其在组织代码中的作用。适合初学者深入理解Kotlin面向对象编程的核心概念。
28 3
|
18天前
|
Java 编译器 Kotlin
Kotlin学习笔记 - 数据类型
《Kotlin学习笔记 - 数据类型》是Kotlin编程语言学习系列的一部分,专注于Kotlin中的数据类型,包括布尔型、数字型(整型和浮点型)、字符型及字符串型,详述了各类型的定义、使用方法及相互间的转换规则。适合初学者快速掌握Kotlin基础语法。
24 3
|
18天前
|
安全 IDE Java
Kotlin 学习笔记- 空类型和智能类型转换
Kotlin 学习笔记聚焦于空类型和智能类型转换,深入解析非空与可空类型、安全调用操作符、Elvis 运算符、非空断言运算符及智能类型转换等内容,助你高效掌握 Kotlin 语言特性,避免 NullPointException 异常,提升代码质量。
25 2
|
20天前
|
Java 编译器 Kotlin
Kotlin学习笔记 - 数据类型
Kotlin学习笔记 - 数据类型
43 4
|
20天前
|
Java 开发者 Kotlin
Kotlin学习笔记- 类与构造器
Kotlin学习笔记- 类与构造器
27 3
|
20天前
|
设计模式 Java Kotlin
Kotlin学习笔记 - 改良设计模式 - 迭代器模式
Kotlin学习笔记 - 改良设计模式 - 迭代器模式
24 2
|
20天前
|
安全 IDE Java
Kotlin 学习笔记- 空类型和智能类型转换
Kotlin 学习笔记- 空类型和智能类型转换
43 2
|
21天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
16 1
|
20天前
|
设计模式 JavaScript Scala
Kotlin学习笔记 - 改良设计模式 - 责任链模式
Kotlin学习笔记 - 改良设计模式 - 责任链模式
42 0
|
20天前
|
设计模式 Java Kotlin
Kotlin 学习笔记- 改良设计模式 - 装饰者模式
Kotlin 学习笔记- 改良设计模式 - 装饰者模式
24 0