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

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

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

目录
相关文章
|
20天前
|
调度 开发者 UED
Kotlin 中的协程是什么?
【8月更文挑战第31天】
37 0
|
3月前
|
存储 Java 调度
Android面试题之Kotlin 协程的挂起、执行和恢复过程
了解Kotlin协程的挂起、执行和恢复机制。挂起时,状态和上下文(局部变量、调用栈、调度器等)被保存;挂起点通过`Continuation`对象处理,释放线程控制权。当恢复条件满足,调度器重新分配线程,调用`resumeWith`恢复执行。关注公众号“AntDream”获取更多并发知识。
71 2
|
4月前
|
移动开发 Android开发 开发者
构建高效Android应用:Kotlin与协程的完美融合
【5月更文挑战第25天】 在移动开发的世界中,性能和响应性是衡量应用质量的关键指标。随着Kotlin的流行和协程的引入,Android开发者现在有了更强大的工具来提升应用的性能和用户体验。本文深入探讨了Kotlin语言如何与协程相结合,为Android应用开发带来异步处理能力的同时,保持代码的简洁性和可读性。我们将通过实际案例分析,展示如何在Android项目中实现协程,以及它们如何帮助开发者更有效地管理后台任务和用户界面的流畅交互。
|
4月前
|
移动开发 数据库 Android开发
构建高效Android应用:探究Kotlin的协程优势
【5月更文挑战第22天】随着移动开发技术的不断进步,Android平台的性能优化已经成为开发者关注的焦点。在众多提升应用性能的手段中,Kotlin语言提供的协程概念因其轻量级线程管理和异步编程能力而受到广泛关注。本文将深入探讨Kotlin协程在Android开发中的应用,以及它如何帮助开发者构建出更高效、响应更快的应用,同时保持代码的简洁性和可读性。
|
4月前
|
移动开发 Android开发 开发者
构建高效安卓应用:Kotlin 协程的实践指南
【5月更文挑战第18天】 随着移动开发技术的不断进步,安卓平台亟需一种高效的异步编程解决方案来应对日益复杂的应用需求。Kotlin 协程作为一种新兴的轻量级线程管理机制,以其简洁的语法和强大的功能,成为解决这一问题的关键。本文将深入探讨Kotlin协程在安卓开发中的实际应用,从基本概念到高级技巧,为开发者提供一份全面的实践指南,旨在帮助读者构建更加高效、稳定的安卓应用。
|
4月前
|
架构师 网络协议 算法
Android高级架构师整理面试经历发现?(大厂面经+学习笔记(1)
Android高级架构师整理面试经历发现?(大厂面经+学习笔记(1)
|
4月前
|
移动开发 安全 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第17天】 在移动开发领域,性能优化和流畅的用户体验是关键。对于Android平台而言,Kotlin语言凭借其简洁性和功能安全性成为开发的首选。与此同时,协程作为一种新的并发处理方式,在简化异步编程方面展现出巨大潜力。本文将深入探讨如何通过Kotlin语言以及协程技术,提升Android应用的性能和响应能力,并确保用户界面的流畅性。
|
4月前
|
移动开发 数据处理 Android开发
构建高效Android应用:Kotlin的协程与Flow的使用
【5月更文挑战第23天】 在移动开发领域,性能优化和异步编程一直是核心议题。随着Kotlin语言在Android开发中的普及,其提供的协程(coroutines)和流式编程(Flow)功能为开发者带来了革命性的工具,以更简洁、高效的方式处理异步任务和数据流。本文将深入探讨Kotlin协程和Flow在Android应用中的实际应用,以及它们如何帮助开发者编写更加响应迅速且不阻塞用户界面的应用程序。我们将通过具体案例分析这两种技术的优势,并展示如何在现有项目中实现这些功能。
|
Java
Kotlin从入门到放弃(三)——协程
引言 这篇主要是将以下kotlin里面的协程,当然这个概念已经随着kotlin的文档被广泛得知了,不过还是用大量代码记录一下吧 一、概念    Coroutine,翻译为协程,意思为各个子任务程协作运行。
2252 0