阅读 300

Android协程(Coroutines)系列-入门

小知识,大挑战!本文正在参与“  程序员必备小知识  ”创作活动

本文同时参与 「掘力星计划」  ,赢取创作大礼包,挑战创作激励金

📕什么是协程?

官方描述:协程(英文Coroutines)通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程就像非常轻量级的线程

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码协程是在版本 1.3 中添加到 Kotlin 的,它基于来自其他语言的既定概念。

协程起源

「协程 Coroutines」源自 Simula 和 Modula-2 语言,这个术语早在 1958 年就被 Melvin Edward Conway 发明并用于构建汇编程序,说明协程是一种编程思想,并不局限于特定的语言。

哪些语言应用到了协程?

  • Lua语言

    Lua从5.0版本开始使用协程,通过扩展库coroutine来实现。

  • Python语言

    正如刚才所写的代码示例,python可以通过 yield/send 的方式实现协程。在python 3.5以后, async/await 成为了更好的替代方案。

  • Go语言

    Go语言对协程的实现非常强大而简洁,可以轻松创建成百上千个协程并发执行。

  • Java语言

    如上文所说,Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,有兴趣的小伙伴可以看一看Kilim框架的源码:github.com/kilim/kilim

协程优点

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。**挂起比阻塞节省内存,且支持多个并行操作。

  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。

  • 内置取消支持取消操作会自动在运行中的整个协程层次结构内传播。

  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

🌴举例说明:

异步代码 & 回调地狱

比如我们有个需求,发起了一个异步请求,从服务端查询用户的信息,通过 CallBack 返回 response:

        getUserInfo(new CallBack()) {
            @Override
            public Void onSuccess (String response){
                if (response != null) {
                    System.out.println(response);
                }
            }
        }
复制代码

如果现在需求修改了,查询用户信息→查找该用户的好友列表→查找该好友的动态(代码看起来非常臃肿)

graph TD
查询用户信息 --> 查找该用户的好友列表--> 查找该好友的动态
//查询用户信息
getUserInfo(new CallBack() {
    @Override
    public Void onSuccess(String user) {
        if (user != null) {
            System.out.println(user);
            //查找该用户的好友列表
            getFriendList(user, new CallBack() {
                @Override
                public Void onSuccess(String friendList) {
                    if (friendList != null) {
                        System.out.println(friendList);
                        //查找该好友的动态
                        getFeedList(friendList, new CallBack() {
                            @Override
                            public Void onSuccess(String feed) {
                                System.out.println(feed);
                            }
                        });
                    }
                });
            }
        }
    }
});
复制代码

地狱到天堂(使用协程Coroutines)

val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)
复制代码

是不是简洁到了极致?这就是 Kotlin 协程的魅力:以同步的方式完成异步任务。

为了更好的理解,在使用协程之前,再讲解一下线程和进程

什么是进程和线程?

在这里插入图片描述

进程是什么呢?

直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。 进程拥有代码和打开的文件资源、数据资源、独立的内存空间。

线程又是什么呢?

线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。 线程拥有自己的栈空间。

对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。 无论进程还是线程,都是由操作系统所管理的。

Java中线程具有五种状态

在这里插入图片描述

线程不同状态之间的转化是谁来实现的呢?是JVM吗?

并不是。JVM需要通过操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。 在 Java 的 API 中,Thread 类是实现线程最基本的类,每创建一个 Thread 对象,就代表着在操作系统内核启动了一个线程,如果我们阅读 Thread 类的源码,可以发现,它的内部实现是大量的 JNI 调用,因为线程的实现必须由操作系统直接提供支持。

线程和协程的关系

在这里插入图片描述

  • 轻量级、高效

    线程是由系统调度的,线程切换或线程阻塞的开销都比较大

  • 依赖于线程

    不能脱离线程而运行

  • 挂起时不阻塞线程

    协程底层库也是异步处理阻塞任务,但是这些复杂的操作被底层库封装起来,协程代码的程序流是顺序的

  • 一个线程可创建任意个协程

  • 不同的线程之间切换

  • 协程可控

    协程底层库也是异步处理阻塞任务,但是这些复杂的操作被底层库封装起来,协程代码的程序流是顺序的

🌼Coroutines开始使用

环境准备

Android中使用Kotlin协程,Kotlin1.3版本以下,需引入kotlinx.coroutines库:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x'//1.3一下需手动引入
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x'
复制代码

Kotlin1.3版本正式支持协程以后,已将kotlinx-coroutines-core库整合到Kotlin语言包中。 包大小:

  • kotlinx-coroutines-core: 0.9M

  • kotlinx-coroutines-android: 21K

所以在我们现在使用Kotlin开发的Android项目中引入kotlinx-coroutines-android库,包体积只会增加 21K,即可忽略不计。

创建协程的几种方式

方式作用
launch:Job创建一个不会阻塞当前线程、没有返回结果的Coroutine,但会返回一个Job对象,Job可以控制这个Coroutine的执行和取消
runBlocking:T创建一个会阻塞当前线程的Coroutine,常用于单元测试的场景,开发一般用不到
async/awit:Deferredasync返回了一个Deferred接口,Deferred接口继承与Job

使用方式一:launch:Job(最常用,不阻塞)

fun main() {
    println("主线程开始")
    
    //作用域可以用使用GlobalScope(不推荐使用)、lifecycleScope、viewModelScope等,
    //这里使用GlobalScope只是方便演示
    val job = GlobalScope.launch(Dispatchers.Default) {//返回Job对象
        repeat(5) {
            println("launch协程:" + Thread.currentThread())
            delay(1000)
        }
    }

    println("主线程结束")
    //这里的sleep只是保持进程存活, 目的是为了等待协程执行完
    Thread.sleep(8000L)
}


//Job中常用的方法
job.isActive
job.isCancelled
job.isCompleted
job.cancel()
job.join()
复制代码

执行main函数,打印结果:

结果显示没有阻塞当前线程(主线程结束后,协程还在继续打印日志)

主线程开始
主线程结束

launch协程:Thread[DefaultDispatcher-worker-1,5,main]
launch协程:Thread[DefaultDispatcher-worker-1,5,main]
launch协程:Thread[DefaultDispatcher-worker-1,5,main]
launch协程:Thread[DefaultDispatcher-worker-1,5,main]
launch协程:Thread[DefaultDispatcher-worker-1,5,main]

Process finished with exit code 0
复制代码

👉使用不同作用域需要添加对应的扩展函数

//使用lifecycleScope
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
//使用viewModelScope
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
复制代码

使用方式二:runBlocking(阻塞)

fun main() {
    println("主线程开始")

    val any = runBlocking(Dispatchers.Default) {
        repeat(5) {
            println("runBlocking协程:" + Thread.currentThread())
            delay(1000)
        }
    }

    println("主线程结束")
    //这里的sleep只是保持进程存活, 目的是为了等待协程执行完
    //Thread.sleep(8000L)
}
复制代码

执行main函数,打印结果:

结果显示阻塞了当前线程(协程打印日志结束后,主线程才结束)

主线程开始

runBlocking协程:Thread[main,5,main]
runBlocking协程:Thread[main,5,main]
runBlocking协程:Thread[main,5,main]
runBlocking协程:Thread[main,5,main]
runBlocking协程:Thread[main,5,main]

主线程结束
复制代码

使用方式三:async/await:Deferred(不阻塞)

fun main() {
    println("主线程开始")

    GlobalScope.launch {
        //开始时间
        val startTime = System.currentTimeMillis()

        //模拟异步获取请求1
        val result1 = async {
            getResult1()
        }
        //模拟异步获取请求2
        val result2 = async {
            getResult2()
        }
        //合并2者请求结果
        val result = result1.await() + "+" + result2.await()
        println("result =    $result")

        //结束时间
        val endTime = System.currentTimeMillis()

        println("time =    ${endTime - startTime}")
    }

    println("主线程结束")
    //这里的sleep只是保持进程存活, 目的是为了等待协程执行完
    Thread.sleep(8000L)

}

//延迟3s获取结果
suspend fun getResult1(): String {
    delay(3000)
    return "result1"
}

//延迟4s获取结果
suspend fun getResult2(): String {
    delay(4000)
    return "result2"
}
复制代码

执行main函数,打印结果:

结果显示没有阻塞当前线程,使用async节约了请求时间(正常需要3s+4s的时间,实际用时4s多一点点)

主线程开始
主线程结束
result =    result1+result2
time =    4010

Process finished with exit code 0
复制代码

协程launch方法

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
复制代码

launch()CoroutineScope的一个扩展函数,CoroutineScope简单来说就是协程的作用范围。launch方法有三个参数:1.协程下上文CoroutineContext;2.协程启动模式CoroutineStart;3.协程体: block是一个带接收者的函数字面量,接收者是CoroutineScope

关键字

协程中涉及到很多关键字,接下来的文章我们会深入理解介绍

  • suspend(挂起函数):挂起,就是一个稍后会被自动切回来的线程调度操作
  • CoroutineStart(启动模式):一共四种启动模式(DEFAULT, LAZY, ATOMIC, UNDISPATCHED;)
  • CoroutineScope(协程作用域):用来指定协程的作用域,可以管控协程
  • Coroutine context(协程上下文):协程上下文,协程上下文里是各种元素的集合
  • Coroutine dispatchers :协程调度,可以指定协程运行在 Android 的哪个线程里
  • Job: 任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期
文章分类
Android
文章标签