android kotlin 协程(二)

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

tips:前面几篇全都是协程的根本运用,没有源码,等后面对协程有个根本理解之后,才会简单的剖析一下源码!

上一篇(android kotlin coroutine 根本入门)

看完本篇你能学会什么:

  • CoroutineDispatcher // 协程调度器 用来切换线程

  • CoroutineName // 协程姓名

  • CoroutineStart // 协程发动形式

  • CoroutineException // launch / async 捕获反常

  • GlobalCoroutineException // 大局捕获反常

CoroutineDispatcher 协程调度器

界说: 依据姓名也能够看出来, 协程调度器, 首要用来切换线程,首要有4种

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

官方文档介绍

先来看一个简单的例子:

android kotlin 协程(二)  基本入门2

这行代码的意思是敞开一个协程,他的作用域在子线程上

能够看出,只需设置DIspatchers.IO 就能够切换线程

tips: 这儿我运用的是协程调试才能够打印出协程编号

1.

android kotlin 协程(二)  基本入门2

  1. -Dkotlinx.coroutines.debug

    android kotlin 协程(二)  基本入门2

运用协程DIspatcher切换线程的时分,需求留意的是,子协程假如调度了,就运用调度后的线程,假如没有调度,始终保持和父协程相同的线程

这儿的调度便是指的是否有DIspatcher.XXX

例如这样:

android kotlin 协程(二)  基本入门2

对于coroutine#4,他会跟从 coroutine#3 的线程

coroutine#3 会 跟从 coroutine#2 的线程

coroutine#2 有本身的调度器IO,所以悉数都是IO线程

再来看一段代码:

android kotlin 协程(二)  基本入门2

withContext() 是用来切换线程,这儿切换到主线程,但是输出的成果并没有切换到主线程

withContext{} 与launch{} 调度的差异:

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

这儿我感觉是kotlin对JVM支撑还不够

由于本身JVM渠道就没有Main线程,Main线程是对与Android渠道的

所以咱们将这段代码拿到android渠道试一下

android kotlin 协程(二)  基本入门2

能够看出,能够切换,咱们以android渠道为主!

这儿需求留意的是:

JVM渠道上没有Dispatcher.Main, 由于Main仅仅针对android的,所以假如想要在JVM渠道上切换Main线程,

需求添加:

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

而且在dispatcher.Main之前调用 Dispatchers.setMain(Dispatchers.Unconfined)

gitHub issues

现在咱们知道了经过Dispatcher.XXX 就能够切换线程, 那么Dispatcher.XXX是什么呢? 这儿以Dispatcher.IO为例

android kotlin 协程(二)  基本入门2

能够看出,承继关系为:

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

终究都是 CoroutineContext 的子类!

完好代码

CoroutineName 协程姓名

**界说:**协程姓名, 子协程会承继父协程的姓名, 假如协程种有自己的姓名,那么就优先运用自己的

android kotlin 协程(二)  基本入门2

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

android kotlin 协程(二)  基本入门2

能够看出,CoroutineName也是CoroutineContext的子类, 假如说

现在咱们现在想要切换到子线程上咱们该怎么做?

经过方才的代码,咱们知道DIspatcher.XXX 其实质便是CoroutineContext, 那么咱们就能够经过内置的操作符重载来完结两个功用的一起操作

android kotlin 协程(二)  基本入门2

完好代码

CoroutineStart 协程发动形式

界说: coroutineStart 用来操控协程调度器,以及协程的履行时机等

  • CoroutineStart.DEFAULT: 当即依据其上下文安排协程履行;
  • CoroutineStart.LAZY: 懒加载,不会当即履行,只要调用的时分才会履行
  • CoroutineStart.ATOMIC: 常合作Job#cancel()来运用, 假如协程体中有新的挂起点,调用Job#cancel()时 撤销挂起点之后的代码,不然悉数撤销
  • CoroutineStart.UnDISPATCHED: 不进行任何调度,包含线程切换等, 线程状况会跟从父协程保持一致

官方参阅

CoroutineStart.DEFAULT 我相信不必过多赘述, 默许便是这个,直接从 CoroutineStart.LAZY开端

CoroutineStart.LAZY

首要来看一段代码:

android kotlin 协程(二)  基本入门2

能够经过这段代码发现, 其他的协程都履行了,只要采用CoroutineStart.LAZY的协程没有履行,而且runBlocking 会一向等待他履行

那么只需求调用Job#start() 或者 job#join() 即可

android kotlin 协程(二)  基本入门2

CoroutineStart.ATOMIC

tips:该属性现在还在试验阶段

先来看正常作用:

android kotlin 协程(二)  基本入门2

在这段代码中,咱们敞开了一个协程,然后当即cancel了,协程中的代码没有履行

假如改成 CoroutineStart.ATOMIC 会发生什么情况呢?

android kotlin 协程(二)  基本入门2

能够惊讶的发现,竟然撤销协程没有作用!

那么这个CoroutineStart.ATOMIC到底有什么用呢?

再来看一段代码:

android kotlin 协程(二)  基本入门2

能够看出, CoroutineStart.ATOMIC 会将挂起点之后的代码给cancel掉,

即便这儿delay好久,也会当即cancel

再换一种挂起点方式

android kotlin 协程(二)  基本入门2

也仍是相同的成果.

Coroutine.UNDISPATCHED

界说: 不进行任何调度,包含线程切换等, 线程状况会跟从父协程保持一致

首要仍是看默许状况

android kotlin 协程(二)  基本入门2

留意:这儿代码会首要履行:1.main start2. main end

这儿有一个调度的概念,比较抽象:

android kotlin 协程(二)  基本入门2

协程始终都是异步履行的,kotlin协程的底层也是线程, kotlin协程说白了便是一个线程框架,

所以创立协程的时分,其实便是创立了一个线程, 运用线程的时分,咱们会经过Thread#start() 告诉JVM咱们有一个任务需求履行,

然后JVM去分配,最终JVM去履行

这儿调度的大致逻辑和线程相似

只不过协程能够轻易的完结2个线程之前切换,切换回来的过程在协程中咱们叫它康复

这儿扯的有点远,先来看本篇的内容 :)

咱们来看看 Coroutine.UNDISPATCHED有什么作用

android kotlin 协程(二)  基本入门2

能够看出,一旦运用了这种发动形式, 就没有了调度的概念,即便是切换线程(withContext)也杯水车薪

跟从父协程线程状况而改变

android kotlin 协程(二)  基本入门2

说实话,这种发动形式我认为比较鸡肋,和不写这个协程如同也没有很大的差异

完好代码

CoroutineException 协程反常捕获

要点: 协程反常捕获有必要放在最顶层的协程作用域上

最简单的咱们经过try catch 来捕获,这种方法就不说了,

首要咱们来看看 coroutineException的承继关系

android kotlin 协程(二)  基本入门2

CoroutineExceptionHandler => AbstractCoroutineContextElement => Element => CoroutineContext

终究承继自 CoroutineContext

到现在为止,咱们知道了 coroutineContext有4个有用的子类

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

首要咱们来剖析 CoroutineScope#launch 反常捕获

捕获反常之前先说一个隐秘: Job不只能够用来操控协程生命周期,还能够用不同的Job 来操控协程的反常捕获

Job合作CoroutineHandler 反常捕获

先来看一段简单的代码:

tip: 假如不写Job 默许便是Job()

android kotlin 协程(二)  基本入门2

能够看出,现在的状况是协程1出现过错之后,就会反馈给CoroutineExcetionHandler

然后协程2就不会履行了

SupervisorJob()

假如有一个场景,咱们需求某个子协程出现问题就出现问题,不应该影响到其他的子协程履行,那么咱们就能够用 SupervisorJob()

SupervisorJob() 的特点便是:假如某个子协程出现问题不会影响兄弟协程

android kotlin 协程(二)  基本入门2

Job与 SupervisorJob 的差异也很明显

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

假如现在场景变一下,现在换成了子协程中出现问题,来看看作用

android kotlin 协程(二)  基本入门2

能够看出, 子协程2并没有履行 这是默许作用,若在子协程中敞开多个子协程,其实主张写法是这样的

coroutineScope{}

android kotlin 协程(二)  基本入门2

为什么要这么写呢? 明明我不写作用就相同,还得写这玩意,不是闲的没事么

我感觉,作用首要便是一致代码,传递CoroutineScope 例如这样

android kotlin 协程(二)  基本入门2

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

现在场景又调整了, 方才是子协程出现问题当即终止子协程的兄弟协程

现在调整成了: 某个子协程出现问题,不影响子协程的兄弟协程,就想 SupervisorJob() 类型

superiverScope{}

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

android kotlin 协程(二)  基本入门2

作用很简单

这儿首要要分清楚

SuperiverScope() 和 superiverScope{} 是不相同的

  • SuperiverScope() 是用来操控兄弟协程反常的,而且他是一个
  • superiverScope{} 是用来操控子协程的兄弟协程的,他是一个函数

async捕获反常

要点: async运用 CoroutineExceptionHandler 是捕获不到反常的

例如这样:

android kotlin 协程(二)  基本入门2

async 的反常在 Deferred#await()中, 还记得上一篇中咱们聊过 Deferred#await()这个方法会获取到async{} 中的回来成果

假如咱们想要捕获async{} 中的反常,咱们只需求try{} catch{} await即可,例如这样写

android kotlin 协程(二)  基本入门2

async 也能够合作 SupervisorJob() 到达子协程出现问题,不影响兄弟协程履行,例如这样:

android kotlin 协程(二)  基本入门2

怎么让 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

完好代码

GlobalCoroutineException 大局反常捕获

需求在本地装备一个捕获监听:

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

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

android kotlin 协程(二)  基本入门2

完好代码

下一篇预告:

  • 协程履行流程 [入门理解挂起与康复]
  • delay() 与 Thread#sleep() 差异

原创不易,您的点赞便是对我最大的支撑!