android kotlin 协程(一) 基础入门

2,246 阅读6分钟

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

前言:最近系统的学习了一遍协程, 计划通过10篇左右blog来记录一下我对协程的理解, 从最简单的 runBlocking开始; 到最后 suspend和continuation的关系等等

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

学习我这个系列的协程, 只需要记住一点, suspend函数 永远不会阻塞main线程执行! 永远是异步的!

看完本篇你将会学到哪些知识:

  • runBlocking()
  • CoroutineScope#launch()
  • CoroutineScope#async()
  • Job的常用方法
  • 协程状态[isActive,isCancelled,isCompleted]

runBlocking

定义: runBlocking 会阻塞线程来等待自己子协程执行完, 并且对于不是子协程的作用域,也会尽量的去执行,

首先来了解一下什么是自己的子协程

image-20230209134504138

通常我们通过

  • CoroutineScope.launch{}
  • CoroutineScope.async{}

来开启一个协程,因为当前是在CoroutineScope作用域中,所以直接launch / async 即可

这段代码可以看出,runBlocking 会等待子协程全部执行完,然后在结束任务,因为协程都是异步的,

所以会先执行协程之外的代码,然后再执行协程中的代码

可以在协程中添加一些睡眠操作再来测试一下

image-20230209135225296

可以看出,还是可以正常的执行完所有代码

现在解释完了定义中的前半句话: runBlocking 会阻塞线程来等待自己子协程执行完, 并且对于不是子协程的作用域,也会尽量的去执行,

再来看一下后半句话:

image-20230209135639421

可以看出,通过自定义coroutine 和 GlobalScope,来创建的协程照样可以执行出来

那么在他们之中稍微添加一点逻辑会怎么样?

image-20230209140524725

可以看出,一旦添加了一点逻辑, runBlocking是不会等待非子协程的作用域

如果想让runBlocking等待非子协程执行完,那么只需要调用Job.#join() 即可

例如这样:

image-20230209141429449

join()方法目前可以理解为: 等待当前协程执行完 在往下执行其他代码,

一旦调用了join()方法,那么协程就变成了同步的,那么这块代码一共执行需要4s

因为协程1并没有join, 所以协程1还是异步的,

协程2调用了join,所以在执行协程2的过程中,协程1也在执行.

所以协程1,与协程2的执行时间为2s

image-20230209142033645

tips: 在开发中不建议使用runBlocking,因为会阻塞主线程,阻塞主线程的时间,用来子协程的执行..

开启协程两种不同的方式

在上面代码中我们提到了,开启协程有2种方式

  • CoroutineScope#launch{}
  • CoroutineScope#async{}

先来看相同点:

image-20230209143252217

相同点就是无论是哪种方式,都会执行里面的代码

那么这两种方式有什么区别呢?

  • launch无法返回数据, async可以返回结果

    image-20230209143141023

返回的结果通过 Deferred#await() 来获取,并且调用Deferred#await()的时候,会等待async{} 执行完成之后在往下执行,就和Job#join一样,不过await()有返回结果

使用await的时候有一个注意点:

image-20230209143832540

那么也可以看到,launch{} 与 async{} 的返回值也有所不同:

  • launch{} 返回 Job
  • async{} 返回Deferred

其实本质上来说,async 返回的也是Job,不过只是Job的子类Deferred而已,Deferred只是对返回值等一些操作的封装

image-20230209144036904

那么Job是用来干什么的呢?

Job是用来管理协程的生命周期的, 例如刚才提到的 Job.join() 就可以让协程 “立即执行”

launch{} 和 async{} 捕获异常的方式也不同,这个等下一篇专门聊异常的时候在详细讲解

Job.cancel 取消协程

协程比线程好用的一点就是协程可以自己管理生命周期, 而线程则不可以

image-20230209145025542

这里需要注意的是,如果协程体中还在执行,但是外部已经取消了,那么则会throw异常出来

JobCancellationException

fun main() = runBlocking<Unit> {
    println("main start")

    val job = launch {
        try {
            (0..100).forEachIndexed { index, _ ->
                delay(1000)
                println("launch $index")
            }
        } catch (e: Exception) {
            println("协程被取消了 $e")
        }
    }

    // 协程执行完成监听
    job.invokeOnCompletion {
        println("协程执行完毕${it}")
    }

    delay(5500)

    // 取消协程
    job.cancel()

    println("main end")
}

Job#invokeOnCompletion: 协程完成回调

运行结果:

main start
launch 0
launch 1
launch 2
launch 3
launch 4
main end
协程被取消了 kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@76a4d6c
协程执行完毕kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelled}@76a4d6c

coroutine的3种状态

coroutine的三种状态都是通过Job来管理的:

  • isActive 是否是活跃状态
  • isCancelled 是否取消
  • isCompleted 是否完成

先来看看正常流程执行的代码:

image-20230209151017683

我们知道协程始终是异步执行的,在执行printlnJob的时候,协程体中的代码还没有真正的执行

所以此时处于活跃状态,并且协程没有被执行完

如果我们在协程执行完成的回调中调用

image-20230209151208403

那么此时,协程体中的代码已经执行完了,那么此时就是非活跃状态

还剩一个Job#isCancelled 这个方法比较简单,简单的说就是是否调用了Job.cancel()

image-20230209151653026

但是这里有一个特别奇怪的点,明明已经调用Job#cancel() 来取消协程,并且协程体中的代码也没执行,但是为什么还显示协程没有执行完呢?

因为Job#cancel() 并不是suspend函数,不是suspend函数就没有恢复功能,这行文字可能看的有一点迷惑,先不用管什么挂起于恢复,现在只需要知道

我们调用cancel() 的时候会紧跟着一个,Job#join() 即可

或者直接调用Job.cancelAndJoin() 即可

image-20230209152145627

挂起恢复,这4个字我理解了10天左右,不可能通过本篇就讲清楚,现在只需要会调用这些api,即可!!

那么问题就来了,这个状态有什么用呢?

先来看一段代码:

Feb-09-2023 15-40-30

可以惊奇的发现,这段代码无论如何都cancel不掉.好像是失效了一样

那么解决这个问题,就可以检测协程是否是活跃状态,例如这样

Feb-09-2023 15-43-37

Job也提供了一个方法: Job#ensureActive()

ensureActive() 本质也是通过isActive判断,不同的是,当取消的时候可以捕获到取消的异常,然后来处理对应的事件

图片地址: gitee.com/lanyangyang…

回顾一下本篇:

本篇我们讲解了runBlocking, 这个函数会帮我们阻塞主线程, 阻塞住线程的时候会等待内部的子协程全部执行完

还聊了最基础的如何开启一个协程, launch / async 以及他们的相同点和不同点

最后引出了协程生命周期管理者Job, 讲解了Job常用的方法,以及job的3种状态

方法名作用补充
join()立即恢复协程体执行等待协程体执行完成,在执行后续代码
cancel()取消协程,如果取消时,协程体还在执行,这throw JobCancellationException,这个异常不会上报,会自行处理
invokeOnCompletion()协程体执行完成回调
isActive协程体是否是活跃状态
isCancelled协程体是否被取消
isCompleted协程体是否执行完成

完整代码

下一篇预告:

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

  • CoroutineName // 协程名字

  • CoroutineStart // 协程启动模式

  • CoroutineException // launch / async 捕获异常

  • GlobalCoroutineException // 全局捕获异常

原创不易,您的点赞就是我最大的支持!