Kotlin进阶指南 - 协程入门

539 阅读9分钟

那段日子抽时间学了一下协程,发现协程其实在几年前就已经存在了,只是近一俩年才开始有了慢慢普及的现象,所以学完后及时输出一下~

不知能否帮到你,但愿别带歪你,时间过得可真是快啊,一晃多年...

协程是什么?

关于协程,其实在Lua语言、Python语言、Go语言、Java语言中都早已存在,Android中是在Kotlin 1.3版本后引入了协程,只是因为当时Kotlin都还没有普及,所以了解协程的人更少了,虽然2018协程已经有了初期稳定版本,但是依旧普及率不高...

协程(coroutines) 是由 JetBrains 开发的丰富的协程库,英语好的同学可以看官网学学基础使用

如果英文不好的话,看看官网中文版的Kotlin协程使用指导吧

在Google中有出过一篇:如何在 Android 应用中使用 Kotlin 协程

话说,android的协程主要体现在Kotlin语言方面,众所周知Kotlin也就是近两三年开始普及的,那么现在掌握协程也是必不可少的技能了

Kotlin协程:我认为Kotlin协程,更多的时候代表的是一个轻量级的线程库或者说是线程框架

初步特征

  • 协程是运行在单线程中的并发程序,意味着它的体量比线程更小
  • 协程支持自动切换线程,子主线程可随意切换

关于协程环境主要涉及到了Dispatchers调度器,常见有三种环境(最后一个个人较少使用- - )

  • Dispatchers.Main:调用程序在Android 中的主线程
  • Dispatchers.IO:适合主线程之外的操作,主要针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算,json数据的解析,以及列表的排序,
  • Dispatchers.Unconfined:在调用的线程直接执行

协程 - 启动模式(枚举)

public enum class CoroutineStart {
    DEFAULT,
    LAZY,
    @ExperimentalCoroutinesApi
    ATOMIC,
    @ExperimentalCoroutinesApi
    UNDISPATCHED;
}

四种启动模式,含义如下

  • DEFAULT:默认的模式,立即执行协程体
  • LAZY:只有在需要的情况下运行
  • ATOMIC:立即执行协程体,但在开始运行之前无法取消
  • UNDISPATCHED:立即在当前线程执行协程体,直到第一个 suspend 调用

为什么要使用协程?

关于协程的特点,Google早有说明

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

话说,协程在写法上允许在不同线程的代码,写在同一个代码块中,还是比较方便的 (这点比较符合内存泄露更少的描述)~

可能很多人都会有一些和我一样的疑问 - 如果只是线程框架的话,为何不继续使用Thread?如果只是为了方便线程切换的话,为何继续使用RxJava?

  • 协程是基于线程的,意味着协程体量比线程要小(看下图秒懂),但是关于性能提升,并不明显;
  • 协程提供了专属的Dispatchers可满足不同场景的线程使用,可及时切换线程;
  • 协程隶属Jetpack组件库,首先Jetpack组件库是Google首推,同时Jetpack的组件被使用率很高
  • 协程兼容了LifecycleViewModelLiveData等组件库,现在这些组件库已经都开始支持协程的使用了

这里借用一下网图,说明线程和协程运行的环境

线程运行环境 在这里插入图片描述 协程运行环境

在这里插入图片描述


如何使用协程?

如果你准备开始使用协程的话,最好是有一定的Kotlin基础,同时对Jetpack相关组件的了解,它会使你事半功倍

开启协程的方式通常有俩种,其一是launch函数,其二是async函数

  • launch 更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。
  • async 则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过 await() 函数获取返回值。

我们可以在协程中动态切换对应任务的执行环境,主要是通过withContext(Dispatchers.环境)方式

协程需要运行在协程上下文环境,在非协程环境中凭空启动协程 - 三种方式

  • runBlocking 建立新的协程,运行在当前线程上,因此会堵塞当前线程,直到协程体结束
  • GlobalScope.launch 启动一个新的线程,在新线程上建立运行协程,不堵塞当前线程
  • GlobalScope.asyn 启动一个新的线程,在新线程上建立运行协程,而且不堵塞当前线程,支持 经过await获取返回值

协程中的任务如何挂起和恢复?

协程中进行协程切换的场景,主要涉及到suspendresume,意图是在挂起函数执行完毕之后,协程会自动的重新切回它原先的线程(注意:普通函数没有suspendresume这两个特性)

关于suspend挂起函数,更多是作为一个标记和提醒,提醒调用者我是需要耗时操作,需要用挂起的方式,在协程中使用放在后台执行

  • suspend 用于暂停执行的当前协程,并保存所有的局部变量
  • resume 用于已暂停的协程中暂停出恢复

add 依赖

    //新版本,慎用,不知有没有坑,介意的话可以使用1.1.1版本
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'

先讲解launch的使用

  • 在主线程中通过GlobalScope.launch(Dispatchers.Main) {}启动协程,这里要注意我们一般将上下文环境设为Dispatchers.Main
  • 关于子线程(IO线程)执行函数,我们首先需要用suspend进行修饰为挂起函数,同时在内部通过withContext进行线程切换

主要使用GlobalScope.launch函数开启全局范围的协程,而其参数我们一般使用的是Dispatchers.Main,意味着主线程协程

        GlobalScope.launch(Dispatchers.Main) {
           //子线程函数
            ioThread()
            //主线程函数
            mainThread()
        }

一般子线程方法,我们使用suspend挂起函数,结合withContext切换子主线程

   //IO线程执行的挂起函数,内部通过withContext声明内部逻辑在子线程执行,执行完毕后会自动切回主线程
    suspend fun ioThread() {
        withContext(Dispatchers.IO) {
            print("IOThread: ${Thread.currentThread().name}")
        }
    }

像上方的写法你可能感觉不到协程的快感,那么你在看看下方同等代码

        GlobalScope.launch {
            //子线程任务
            withContext(Dispatchers.IO) {
                print("IOThread: ${Thread.currentThread().name}")
            }
            //自动切回主线程
            print("MainThread:+${Thread.currentThread().name}")
            //子线程任务
            withContext(Dispatchers.IO) {
                print("IOThread: ${Thread.currentThread().name}")
            }
        }

在讲解async的使用

首先async执行的协程是支持通过await()返回数据的,同时async也常用于并行任务,我们可以同步执行多个协程任务,最后一起同步返回

协程的suspend挂起函数,除了本身提醒的作用外,一般涵盖着线程切换

        //并发请求
        val asyncLaunch = GlobalScope.launch {
            val async = async { add1() }
            val async1 = async { add2() }
            System.out.println(async.await() + async1.await())
        }

        suspend fun add1(): Int {
            delay(1000L)
            return 10 + 10;
        }

        suspend fun add2(): Int {
            delay(2000L)
            return 5 + 8;
        }

在文档中有一种通过awaitAll批量获取async数据的方式,有兴趣可以学学,简单方便

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

如何避免协程泄露、内存泄露?

首先想一下我们常规是如何避免内存泄漏的?嗯... 有点墨迹了,其实大多是在onDestroy中将组件cancle或将数据设置为null等~

通过launch函数查看内部源码可以发现它会返回一个Job对象,那么我们在往内部看一看

在这里插入图片描述

查看Job对象内部可以看出Job是拥有cancel方法的,那么我们完全可以在组件的onDestroy中取消协程,避免协程内部持续引用外部对象而造成泄露

在这里插入图片描述 故此,我们可以直接获取协程的对象,然后调用cancel的方法,从而防止内存泄露;不过现在使用Lifecycle更便捷一些

		//开启协程
        val job = GlobalScope.launch {
            //子线程任务
            withContext(Dispatchers.IO) {
                print("IOThread: ${Thread.currentThread().name}")
            }
            //自动切回主线程
            print("MainThread:+${Thread.currentThread().name}")
            //子线程任务
            withContext(Dispatchers.IO) {
                print("IOThread: ${Thread.currentThread().name}")
            }
        }
        //取消协程
        job.cancel()

兴趣扩展

Job:协程构建函数的返回值,可以把 Job 看成协程对象本身,协程的操作方法都在 Job 身上了

  • job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动
  • job.join() - 等待协程执行完毕
  • job.cancel() - 取消一个协程
  • job.cancelAndJoin() - 等待协程执行完毕然后再取消

Jetpack AAC 哪些组件支持协程?

谈支持协程的组件前,还是想说一下GlobeScope不受欢迎的原因 - - ~

GlobeScope:生命周期与app同步,随着kotlin的更新,已经慢慢不推荐使用这个了

  • 不推荐的原因:主要是很难避免因自己失误操作,出现的内存泄漏问题

  • 推荐原因:个人认为作为新手入门使用的Scope还是可以的

话说回来,目前来看AAC组件库中的Lifecycle、ViewModel、LiveData都已经开始支持协程的使用了,也都提供了对应的协程调用方式

使用 lifecycleScopeviewModelScope ,最好有 LifecycleViewModel 基础,记得加入以下依赖

    //lifecycleScope
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
    //viewModelScope
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

lifecycleScope:在activity或者fragment里面使用协程的时候,用lifecycleScope,它在Lifecycle执行,onDestory的时候取消

半夜了,有点无聊,我们看看lifecycleScope提供的方法,内部封装了启动协程的生命周期,又一次可以偷懒了..

在这里插入图片描述

LifecycleCoroutineScope 内涵盖方法

在这里插入图片描述

无聊,写个样例,我们可以通过lifecycleScope动态设置启动协程的时间

    val launchWhenCreated = lifecycleScope.launchWhenCreated {
        print("半夜咯")
        suspend { 
            withContext(Dispatchers.IO){
                print("睡觉吧")
            }
        }
        print("晚安")
    }

    override fun onDestroy() {
        super.onDestroy()
        launchWhenCreated.cancel()
    }

viewModelScopeviewModelScope只能在ViewModel里面使用协程,它会在ViewModel调用clear方法的时候取消;这方便近期忙着学习,还没有用到,等以后我回头补全