Kotlin协程-Coroutines的基本使用

·  阅读 1769
Kotlin协程-Coroutines的基本使用

Kotlin协程的基本使用

Kotlin协程系列:

其实网上已经有很多 Kotlin协程 的教程了,这里我出一期是为了记录自己的总结。也是自己的理解与输出。

由于协程的概念是一个比较大的东西,这里我分解成几个不同的模块来讲解,本文是系列的第一期,本系列讲的是协程各模块的偏实战使用,并不会过多的涉及到原理与源码,本文讲的是协程的基本使用。

可以看到这里的协程我加上了前缀,Kotlin协程,系列文章后面所指的协程,都是 Kotlin协程 。都已经是众所周知了,Kotlin协程与其他语言的协程在实现上有所不同。这里我就不过多介绍,不了解的看这里

一、为什么用协程

Android开发使用的是Java语言,Java的线程管理是使用 Thread 。在Android开发中我们多多少少也用到过 Thread

但是我们需要在主线程才能更新UI,在子线程 Thread 中处理完逻辑我们需要调用Api runOnUiThread切换到主线程更新UI,包括Kotlin语法,使用起来都是一样的套路,虽然有了thread的扩展方法,但是内部处理流程是和Java一样的

伪代码如下:

   thread {
        dosth()

        runOnUiThread {
            updateUI()
        }
    }
复制代码

如果逻辑多了,或者说频繁的切换线程,那么就算是Kotlin的语法糖,实现起来也是嵌套很多次,由此我们会使用一些优秀的框架如 RxJava 来管理异步和并发的一些操作,管理切换线程的逻辑。

而得益于Kotlin语言的设计,在1.3版本加入了协程的概念,后期又出了一些Jetpack的组件,天然支持协程,使得协程的概念越来越为人熟知,更多的人开始使用协程了。而我们也就无需使用 RxJava 等第三方框架来管理线程了。

协程的特点:

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 更少的内存泄漏:使用结构化并发机制在一个作用域内执行多项操作
  • 支持取消:取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发

二、怎么使用协程

Kotlin语法是默认不带协程的,如果我们想使用协程还是需要引入协程框架库。注意需要Kotlin版本1.3以上。这里我使用的Kotlin版本为1.4.21。

协程库的引入:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
复制代码

我们就能使用一个简单的协程:

       GlobalScope.launch {
            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")
        }
复制代码

Kotlin中,有几种方式能够启动协程,如下所示:

  • launch{} CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,并返回新协程的Job。

  • async{} CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,返回一个Deffer,除包装了未来的结果外,其余特性与launch{}一致

  • runBlocking{} 是一个裸方法,创建一个协程,并阻塞当前线程,直到协程执行完毕。前面说过,这里不再赘述。

  • withContext(){} 一个suspend方法,在给定的上下文执行给定挂起块并返回结果,它并不启动协程,只会(可能会)导致线程的切换。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。 withContext的目的不在于启动子协程,它最初用于将长耗时操作从UI线程切走,完事再切回来。

  • coroutineScope{} 一个suspend方法,创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解(即,将长耗时任务拆分为并发的多个短耗时任务,并等待所有并发任务完成后再返回)。

可以看到只有前三种方法是创建或启动一个协程的,后面那种方式都是切换线程,或者创建作用域的一个方法。

我们举例说明一下

    coroutineScope {  }  //报错 ,不能直接用,只能在协程里面使用
        runBlocking {
            coroutineScope {  }   //正常使用,因为runBlocking创建了协程
           
             YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")
        }
复制代码
    runBlocking {
            coroutineScope {  //这里包一层也没什么,只是多了一层代码块而已, 不影响逻辑

            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")
            }
        }
复制代码

源码查看

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
复制代码

launch返回的是Job对象,用于控制协程的生命周期

异步的启动

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>
复制代码

async返回的是Deferred用于等待未来结果的返回。一般使用 await 来调用获取结果。

public suspend fun await(): T
复制代码

切换线程withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 
复制代码

内部需要传入一个协程上下文,我们一般使用调度器Dispatchers来切换线程,它是协程上下文CoroutineContext的实现类之一。(后面会单独出一期)

  • Dispatchers.Main Android主线程
  • Dispatchers.Unconfined 当前CoroutineScope的线程策略
  • Dispatchers.Default 默认值,为JVM共享线程池
  • Dispatchers.IO IO线程池,默认为64个线程

通过lauch 和 async 和 withContext 的使用示例如下:

     GlobalScope.launch{
            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            val deferred = async {
                YYLogUtils.w("切换到另一个协程")
                Thread.sleep(2000)
                return@async "response data"
            }

            val response = deferred.await()
            YYLogUtils.w("response:$response")


            val result = withContext(Dispatchers.IO) {
                //异步执行
                delay(1000L)
                YYLogUtils.w("切换到另一个协程")
                return@withContext "1234"
            }

            YYLogUtils.w("result:$result")

            delay(1000L)

            YYLogUtils.w("执行完毕...")

        }
复制代码

这也是我们常用的两种方式:async是异步执行,withContext是同步执行。

当它们的代码块执行完毕,就会回到主协程的线程中,换句话说就是通过调度器实现切换线程,执行完就回到当前线程。如果想知道底层逻辑可以看这里

到这里就会简单的协程使用了,但是注意上面的代码有时候用的 sleep 有时候用的 delay ,看着意思都是延迟的意思,有什么区别?

三、协程的阻塞与挂起

阻塞的意思就是会阻断当前线程后面的代码不会执行。挂起的全名应该叫非阻塞式挂起,其意思是为不会阻塞其他协程,只是当前自己所在协程会挂起等待不执行,但是其他协程还是能继续执行的。

3.1 suspend非阻塞挂起函数

我们使用AS来编程,就很清晰的可以看到,左侧有箭头的就是挂起,而sleep方法是没有箭头的就不是挂起而是阻塞。

而挂起的方法调用都是需要 suspend 标记的,如

public suspend fun delay(timeMillis: Long) {
...
}
复制代码

我们举一个简单的例子说明:

     GlobalScope.launch{
           YYLogUtils.w("执行在协程中...")

           val result1 = withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                return@withContext "1234"
            }

            YYLogUtils.w("result1:$result1")

            val result2 = withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                return@withContext "123456"
            }
            YYLogUtils.w("result2:$result2")

            YYLogUtils.w("执行完毕...")

        }
复制代码

打印Log如下:

这是正常的,顺序执行的。为什么‘阻塞’了?不是说 delay 函数是挂起函数,是非阻塞的吗?OK,再次强调一点,此阻塞的概念并非是说阻塞这个线程,阻塞这段代码不让执行,此阻塞是针对其他 协程 的。上面的 withContext 它创建/启动了了协程吗?没有,它只是切换了线程,它本身其实也是 suspend 的函数而已。所以上面的代码是顺序执行的。

下面我们修改一下代码为启动协程:

     GlobalScope.launch{
           YYLogUtils.w("执行在协程中...")

           GlobalScope.launch(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            GlobalScope.launch(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }

            YYLogUtils.w("执行完毕...")

        }
复制代码

挂起函数是不会阻塞协程的,打印Log如下:

而我们自定义的函数方法,也可以通过标记 suspend 而在协程中使用

GlobalScope.launch{
           YYLogUtils.w("执行在协程中...")

           saveSth2Local()

           val result1 = withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)

                saveSth2Local()

                return@withContext "1234"
            }

            YYLogUtils.w("result1:$result1")

           

            YYLogUtils.w("执行完毕...")

        }

   suspend fun saveSth2Local() {
        DBHelper.get().saveUser()
    }       
复制代码

使用我们定义的 saveSth2Local 挂起方法的时候,在哪个作用域使用就是在哪个线程执行,如上面的 saveSth2Local 方法是在主线程执行,withContext 中的 saveSth2Local 方法则是在子线程中使用。

注:本文是偏实战的使用分享,关于suspend的内部实现原理和源码分析,我想大家多多少少有些概念,如果想了解详细的分析过程,推荐大家看这个文章

3.2 runBlocking阻塞协程

上面我们讲到的是 suspend 挂起函数的阻塞与非阻塞的概念,而我们启动函数launch 和 runBlocking 也是区分阻塞与非阻塞的。概念都是一样的,就是是否阻塞其他协程。

launch的是非阻塞的,runBlocking就是阻塞的,它会阻止其他协程的运行。

同样的代码,我们把 GlobalScope.launch 改为 runBlocking 试试:

    GlobalScope.launch{
          YYLogUtils.w("执行在协程中...")

          runBlocking(Dispatchers.IO) {
               //异步执行
               YYLogUtils.w("异步执行result1")
               delay(1000L)
               YYLogUtils.w("result1:1234")
           }

           runBlocking(Dispatchers.IO) {
               //异步执行
               YYLogUtils.w("异步执行result2")
               delay(1000L)
               YYLogUtils.w("result2:123456")
           }

           YYLogUtils.w("执行完毕...")

       }
复制代码

运行的结果:

可以看到 runBlocking 真的就阻止了其他协程的运行,得自己运行完成了,才能继续运行其他协程。这就是阻塞式的。

不管是不是一个作用域,不管是同级的兄弟协程还是父子协程,都是会被阻塞的。

        CoroutineScope(Dispatchers.Main).launch {
            YYLogUtils.w("执行在协程中...")

            withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            withContext(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }

            delay(1000L)

            YYLogUtils.w("执行完毕...")

        }


        GlobalScope.launch(Dispatchers.Main) {
            YYLogUtils.w("执行在另一个协程中...")

            delay(1000L)

            YYLogUtils.w("另一个协程执行完毕...")
        }
复制代码

如上面的代码,2个父作用域的协程同时执行,一旦第一个协程内部没有被阻塞,那么下面的协程就能执行了,此时2个协程就是并发的,Log如下:

但是我们修改一下代码,把上面的协程阻塞住,那么下面的协程就需要等待阻塞的代码执行完毕才能执行,此时两个协程就是非并发的串联顺序执行的。

        CoroutineScope(Dispatchers.Main).launch {
            YYLogUtils.w("执行在协程中...")

            runBlocking(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result1")
                delay(1000L)
                YYLogUtils.w("result1:1234")
            }

            runBlocking(Dispatchers.IO) {
                //异步执行
                YYLogUtils.w("异步执行result2")
                delay(1000L)
                YYLogUtils.w("result2:123456")
            }

            delay(1000L)

            YYLogUtils.w("执行完毕...")

        }


        GlobalScope.launch(Dispatchers.Main) {
            YYLogUtils.w("执行在另一个协程中...")

            delay(1000L)

            YYLogUtils.w("另一个协程执行完毕...")
        }
复制代码

非并发的打印结果如下:

这样讲解相信大家应该能理解阻塞的概念了。

总结

本文是协程使用系列的第一篇,基本的使用,本篇只涉及基本的概念和基本的使用,上面讲到协程有生命周期,支持取消等其他特性,后面几期会讲到的。

我们在这一期需要掌握的就是协程启动的几种方式,切换线程的几种方式,异步与同步执行的异同,挂起函数,阻塞与非阻塞的概念。

协程的概念与框架比较大,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。

Ok,这一期就此完结。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改