服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Kotlin协程开发的基础入门知识

日期: 来源:郭霖收集编辑:史大拿


/   今日科技快讯   /

近日,爱奇艺官方账号发文,针对近期爱奇艺VIP会员用户反馈的投屏清晰度、设备登录等问题做出如下调整:

1.为2023年2月20日仍处于订阅状态的爱奇艺黄金 VIP 会员,恢复720P和1080P清晰度的投屏服务。

2.为方便用户跨终端使用,从2023年2月20日起,爱奇艺黄金、白金、星钻 VIP 会员可在5台设备上登录,不再限制登录设备种类。在同一时间播放的设备也不再限制种类。

/   作者简介   /

本篇文章来自史大拿的投稿,文章主要分享了Kotlin中协程的基础入门相关知识,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

史大拿的博客地址:
https://juejin.cn/user/2251439606079277

/   前言   /

config

  • system: macOS
  • android studio: 2022.1.1 Electric Eel
  • gradle: gradle-7.5-bin.zip
  • android build gradle: 7.1.0
  • Kotlin coroutine core: 1.6.4

看完本篇你能学会什么:

  • CoroutineDispatcher // 协程调度器,用来切换线程
  • CoroutineName // 协程名字
  • CoroutineStart // 协程启动模式
  • CoroutineException // launch / async 捕获异常
  • GlobalCoroutineException // 全局捕获异常

/   CoroutineDispatcher 协程调度器   /

定义:根据名字也可以看出来,协程调度器,主要用来切换线程,主要有4种。

  • Dispatchers.Main - 使用此调度程序可在Android主线程上运行协程
  • Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用Room组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作
  • Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析JSON
  • Dispatchers.Unconfined - 始终和父协程使用同一线程

官方文档介绍地址如下:
https://developer.android.google.cn/kotlin/coroutines/coroutines-adv?hl=zh-cn#main-safety

先来看一个简单的例子:


这行代码的意思是开启一个协程,他的作用域在子线程上。可以看出只要设置DIspatchers.IO就可以切换线程。

tips:这里我使用的是协程调试才可以打印出协程编号。


-Dkotlinx.coroutines.debug


使用协程DIspatcher切换线程的时候,需要注意的是子协程如果调度了,就使用调度后的线程,如果没有调度,始终保持和父协程相同的线程。这里的调度就是指的是否有DIspatcher.XXX。

例如这样:


对于coroutine#4,他会跟随coroutine#3的线程。coroutine#3会跟随coroutine#2 的线程。coroutine#2有自身的调度器IO,所以全部都是IO线程。

再来看一段代码:


withContext()是用来切换线程,这里切换到主线程,但是输出的结果并没有切换到主线程。withContext{}与launch{}调度的区别:

  • withContext在原有协程上切换线程
  • launch创建一个新的协程来切换线程

这里我感觉是kotlin对JVM支持还不够。因为本身JVM平台就没有Main线程,Main线程是对与Android平台的。所以我们将这段代码拿到android平台试一下。


可以看出,可以切换,我们以android平台为主!这里需要注意的是,JVM平台上没有Dispatcher.Main,因为Main只是针对android的,所以如果想要在JVM平台上切换Main线程,需要添加:

implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")

并且在dispatcher.Main之前调用 Dispatchers.setMain(Dispatchers.Unconfined)。现在我们知道了通过Dispatcher.XXX就可以切换线程,那么Dispatcher.XXX是什么呢?这里以Dispatcher.IO为例。


可以看出,继承关系为:

Dispatcher.IO = DefaultIoScheduler => ExecutorCoroutineDispatcher => CoroutineDispatcher => AbstractCoroutineContextElement => Element => CoroutineContext

最终都是 CoroutineContext 的子类!完整代码地址如下:
https://gitee.com/lanyangyangzzz/coroutine-project/blob/main/app/src/main/java/com/szj/coroutine/project/jvm/blog2/CoroutineDispatcherTest.kt

/   CoroutineName 协程名字   /

定义:协程名字,子协程会继承父协程的名字,如果协程种有自己的名字,那么就优先使用自己的。


这块代码比较简单,就不废话了。


可以看出CoroutineName也是CoroutineContext的子类,如果说现在我们现在想要切换到子线程上我们该怎么做?通过刚才的代码,我们知道DIspatcher.XXX 其本质就是CoroutineContext,那么我们就可以通过内置的操作符重载来实现两个功能的同时操作。


完整代码如下所示:
https://gitee.com/lanyangyangzzz/coroutine-project/blob/main/app/src/main/java/com/szj/coroutine/project/jvm/blog2/CoroutineNameTest.kt

/   CoroutineStart 协程启动模式   /

定义:coroutineStart用来控制协程调度器,以及协程的执行时机等。

  • CoroutineStart.DEFAULT:立即根据其上下文安排协程执行
  • CoroutineStart.LAZY:懒加载,不会立即执行,只有调用的时候才会执行
  • CoroutineStart.ATOMIC:常配合Job#cancel()来使用, 如果协程体中有新的挂起点,调用Job#cancel()时取消挂起点之后的代码,否则全部取消
  • CoroutineStart.UnDISPATCHED:不进行任何调度,包括线程切换等,线程状态会跟随父协程保持一致

官方参考地址:
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/

CoroutineStart.DEFAULT我相信不用过多赘述,默认就是这个,直接从CoroutineStart.LAZY开始。

CoroutineStart.LAZY

首先来看一段代码:


可以通过这段代码发现,其余的协程都执行了,只有采用CoroutineStart.LAZY的协程没有执行,并且runBlocking会一直等待他执行。那么只需要调用Job#start()或者job#join()即可。


CoroutineStart.ATOMIC

tips:该属性目前还在试验阶段

先来看正常效果:


在这段代码中,我们开启了一个协程,然后立即cancel了,协程中的代码没有执行。如果改成CoroutineStart.ATOMIC会发生什么情况呢?


可以惊奇的发现,居然取消协程没有作用!那么这个CoroutineStart.ATOMIC到底有什么用呢?再来看一段代码:


可以看出CoroutineStart.ATOMIC会将挂起点之后的代码给cancel掉,即使这里delay很久,也会立即cancel。再换一种挂起点方式。


也还是同样的结果。

Coroutine.UNDISPATCHED

定义:不进行任何调度,包括线程切换等,线程状态会跟随父协程保持一致。首先还是看默认状态:


注意:这里代码会首先执行main start 和main end。这里有一个调度的概念,比较抽象:


协程始终都是异步执行的,kotlin协程的底层也是线程,kotlin协程说白了就是一个线程框架,所以创建协程的时候,其实就是创建了一个线程,使用线程的时候,我们会通过Thread#start()告诉JVM我们有一个任务需要执行,然后JVM去分配,最后JVM去执行。这里调度的大致逻辑和线程类似,只不过协程可以轻易的实现2个线程之前切换,切换回来的过程在协程中我们叫它恢复。这里扯的有点远,先来看本篇的内容 :)

我们来看看Coroutine.UNDISPATCHED有什么作用。


可以看出,一旦使用了这种启动模式,就没有了调度的概念,即使是切换线程(withContext)也无济于事。跟随父协程线程状态而变化。


说实话,这种启动模式我认为比较鸡肋,和不写这个协程好像也没有很大的区别。

完整代码地址:
https://gitee.com/lanyangyangzzz/coroutine-project/blob/main/app/src/main/java/com/szj/coroutine/project/jvm/blog2/CoroutineStartTest.kt

/   CoroutineException 协程异常捕获   /

重点:协程异常捕获必须放在最顶层的协程作用域上。

最简单的我们通过try catch来捕获,这种办法就不说了。首先我们来看看coroutineException的继承关系。


CoroutineExceptionHandler => AbstractCoroutineContextElement => Element => CoroutineContext

最终继承自CoroutineContext。

到目前为止,我们知道了coroutineContext有4个有用的子类。

  • Job用来控制协程生命周期
  • CoroutineDispatcher协程调度器,用来切换线程
  • CoroutineName写成名字
  • CoroutineException协程异常捕获

首先我们来分析CoroutineScope#launch异常捕获。捕获异常之前先说一个秘密,Job不仅可以用来控制协程生命周期,还可以用不同的Job来控制协程的异常捕获。

Job配合CoroutineHandler异常捕获

先来看一段简单的代码:

tip:如果不写Job默认就是Job()


可以看出目前的状态是协程1出现错误之后,就会反馈给CoroutineExcetionHandler
然后协程2就不会执行了。

SupervisorJob()

假如有一个场景,我们需要某个子协程出现问题就出现问题,不应该影响到其他的子协程执行,那么我们就可以用 SupervisorJob()。SupervisorJob()的特点就是如果某个子协程出现问题不会影响兄弟协程。


Job与SupervisorJob的区别也很明显

  • Job某个协程出现问题,会直接影响兄弟协程,兄弟协程不会执行
  • SupervisorJob某个协程出现问题,不会影响兄弟协程

如果现在场景变一下,现在换成了子协程中出现问题,来看看效果。


可以看出子协程2并没有执行。这是默认效果,若在子协程中开启多个子协程,其实建议写法是这样的。

coroutineScope{}


为什么要这么写呢?明明我不写效果就一样,还得写这玩意,不是闲的没事么。我感觉作用主要就是统一代码,传递CoroutineScope。例如这样:


正常在实际开发中如果吧代码全写到一坨,应该会遭到同行鄙视 :]

现在场景又调整了,刚才是子协程出现问题立即终止子协程的兄弟协程。现在调整成了,某个子协程出现问题,不影响子协程的兄弟协程,就想SupervisorJob()类型。

superiverScope{}

那就请出了我们的superiverScope{}作用域。


效果很简单。这里主要要分清楚SuperiverScope()和superiverScope{}是不一样的。

  • SuperiverScope()是用来控制兄弟协程异常的,并且他是一个类
  • superiverScope{}是用来控制子协程的兄弟协程的,他是一个函数

async捕获异常

重点:async使用CoroutineExceptionHandler是捕获不到异常的。

例如这样:


async的异常在Deferred#await()中,还记得上一篇中我们聊过Deferred#await()这个方法会获取到async{}中的返回结果。如果我们想要捕获async{}中的异常,我们只需要try{}、catch{}、await即可。例如这样写:


async也可以配合SupervisorJob()达到子协程出现问题,不影响兄弟协程执行,例如这样:


如何让CoroutineExceptionHandler监听到async的异常,本质是监听不到的。但是,我们知道了deferred#await()会抛出异常,那么我们可以套一层launch{}。这样一来就可以达到我们想要的效果。

suspend fun main() {
    val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        printlnThread("catch 到了 $throwable")
    }
    val customScope =
        CoroutineScope(SupervisorJob() + CoroutineName("自定义协程") + Dispatchers.IO + exceptionHandler)

    val deferred1 = customScope.async {
        printlnThread("子协程 1 start")
        throw KotlinNullPointerException(" ============= 出错拉 1")
        "协程1执行完成"
    }

    val deferred2 = customScope.async {
        printlnThread("子协程 2 start")
        "协程2执行完成"
    }
    val deferred3 = customScope.async {
        printlnThread("子协程 3 start")
        throw KotlinNullPointerException(" ============= 出错拉 3")
        "协程3执行完成"
    }

    customScope.launch {
        supervisorScope {
            launch {
               val result =  deferred1.await()
                println("协程1 result:$result")
            }
            launch {
                val result =  deferred2.await()
                println("协程2 result:$result")
            }
            launch {
                val result =  deferred3.await()
                println("协程3 result:$result")
            }
        }
    }.join()
}

结果为:

子协程 3 start:     thread:DefaultDispatcher-worker-2 @自定义协程#3
子协程 2 start:     thread:DefaultDispatcher-worker-3 @自定义协程#2
子协程 1 start:     thread:DefaultDispatcher-worker-1 @自定义协程#1
协程2 result:协程2执行完成
catch 到了 kotlin.KotlinNullPointerException:  ============= 出错拉 3:     thread:DefaultDispatcher-worker-2 @自定义协程#7
catch 到了 kotlin.KotlinNullPointerException:  ============= 出错拉 1:     thread:DefaultDispatcher-worker-1 @自定义协程#5

协程捕获异常,最终要的一点就是协程中的异常会一直向上传递。如果想要使用CoroutineExceptionHandler监听到异常,那么就必须将CoroutineExceptionHandler配置到最顶级的coroutineScope中。

完整代码如下:
https://gitee.com/lanyangyangzzz/coroutine-project/blob/main/app/src/main/java/com/szj/coroutine/project/jvm/blog2/CoroutineExceptionTest.kt

GlobalCoroutineException全局异常捕获

需要在本地配置一个捕获监听:

resources/META-INF/services/kotlinx.coroutines.CoroutineExceptionHandler

就和APT类似,如果你玩过APT的话,肯定知道这一步是在做什么。


完整代码如下:
https://gitee.com/lanyangyangzzz/coroutine-project

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Kotlin Flow响应式编程,StateFlow和SharedFlow
深入探究Kotlin的可见性控制,从internal入手

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注


相关阅读

  • Android自定义通知方方面面全适配

  • / 今日科技快讯 /近日,社交媒体推特老板埃隆·马斯克在回复用户推文时表示,推特将于下周开源算法。在此之前,马斯克一直说要将推特算法开源。周二一名推特用户表示,如果“现
  • 推倒重来,如何快速从 0 到 1 重构一个 APP 项目?

  • 说到「重构」这个话题,或许有些朋友遇到过这样的经历,公司早年间上线的项目系统,因一直没专人在演进过程中为代码质量负责,导致现在代码越来越混乱,逐渐堆积成“屎山”,目前的维护
  • 分享两种方式:如何开启JNI的“大门”?

  • ‍‍1要介绍本篇博客的原因前段时间学习OpenGL ES相关技术,下载了一个Github项目学习,项目地址在:https://github.com/githubhaohao/NDK_OpenGLES_3_0项目的关键代码都是C++实
  • 免费开源Windows系统错误代码查询工具!

  • “设为星标”第一时间获取各类实用干货!前言大家在使用Windows系统过程中,或多或少都遇到过一些错误,有些错误系统会给出解释,有些错误则只给出对应的错误代码,而这些错误代码代
  • 老板:你来弄一个团队代码规范!?

  • 点击上方 三分钟学前端,关注公众号面试官也在看的前端面试资料本篇文章讲怎么在前端团队快速制定并落地代码规范!!!干货,拿走这个仓库[1]image.png一、背景9月份换了一个新部
  • 微信 JS-SDK 出发,一起了解JSBridge的神奇功能

  • 前言前段时间由于要实现 H5 移动端拉取微信卡包并同步卡包数据的功能,于是在项目中引入了 **`微信 JS-SDK(jweixin)`**[1] 相关包实现功能,但也由此让我对其产生了好奇心,于是打

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • Kotlin协程开发的基础入门知识

  • / 今日科技快讯 /近日,爱奇艺官方账号发文,针对近期爱奇艺VIP会员用户反馈的投屏清晰度、设备登录等问题做出如下调整:1.为2023年2月20日仍处于订阅状态的爱奇艺黄金 VIP
  • 湖北医药学院举行春季招聘会,提供岗位两万余个

  • 十堰广电讯(全媒体记者 翁红 见习记者 钱心玥 通讯员 鲍晓宇)2月28日,湖北医药学院举行春季招聘会,来自省内外700多家医疗卫生机构,提供两万多个岗位,吸引毕业生求职就业。28日上
  • 拥有思想,你就是高级、资深、专家、架构师

  • / 今日科技快讯 /近日,阿里国际站最新跨境指数显示,过去一年新能源车充电桩的海外商机快速增长了245%,而未来还有将近3倍的需求空间,成为国内外贸企业的新机会。据悉,阿里国
  • 平塘公安“天、地”联动 禁种铲毒不留死角

  • 今年以来,平塘县公安局坚持问题导向、精准施策,强化科技支撑、综合治理,从“早部署+广宣传+深踏查”三个维度发力,高效铲除毒品隐患根源,扎实开展禁种铲毒专项行动,确保实现“零种
  • Android自定义通知方方面面全适配

  • / 今日科技快讯 /近日,社交媒体推特老板埃隆·马斯克在回复用户推文时表示,推特将于下周开源算法。在此之前,马斯克一直说要将推特算法开源。周二一名推特用户表示,如果“现