五分钟技术趣谈 | 试论Android异步框架Kotlin协程

移动开发
协程主要目的是简化异步编程,编写非阻塞代码与编写阻塞代码基本相同,编程模型本身并没有真正改变,解决并发中常见的回调地狱。本文介绍协程的使用及原理剖析。

Part 01

 什么是协程 

作为开发人员尤其是客户端应用开发,我们一直面临着需要解决的问题——如何防止我们的应用程序被阻塞。考虑下面一个异步应用场景。客户端顺序进行3次网络请求,最后更新UI展示结果。

图片图片

图1 异步场景

有多种方法实现上述需求,主流的包括:

  • 回调
  • Rx(反应式扩展)
  • 协程

1.1 回调方式

图片

图2 回调代码示例

异步回调的方式虽然实现了需求,但是这种结构的代码无论是阅读还是维护起来都是极其糟糕的。这种回调函数的层层嵌套耦合,亲切地称为 "回调地狱"。

1.2 Rx方式

图片

图3 Rx代码示例

Rx系列的链式调用,是在协程之前推荐的做法,RxJava丰富的操作符、简便的线程调度、异常处理使得大多数人满意。但是还有没有更简洁易读的写法呢?

1.3 协程方式

图片

图4 协程代码示例

使用协程后的代码非常简洁,以顺序的方式编写异步代码,不会阻塞当前UI线程,错误处理、线程切换也和平常代码一样简单。

协程具有以下几个特点:

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级。
  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
  • 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack集成:许多Jetpack库都提供全面协程支持的扩展。某些库还提供自己的协程作用域,可用于结构化并发。

总而言之:协程可以简化异步编程,可以顺序地表达程序。协程使用挂起,这意味可以在代码的特定点暂停和恢复执行,无需阻塞主线程或显示创建额外的线程。

Part 02

协程的使用 

- 引入gradle依赖

图片

图5 gradle依赖引入

- 启动协程

图片

图6 启动协程

上面就是启动协程的代码,启动协程的代码可以分为三部分:GlobalScope、launch、Dispatchers,它们分别对应:协程的作用域、构建器和调度器。

2.1 协程作用域

指的是协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行。

官方库给我们提供了一些作用域可以直接来使用:

  • runBlocking

顶层函数,但是它会阻塞当前线程,主要用于测试。

  • GlobalScope

全局协程作用域,它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,运行时会消耗一些内存资源,这可能会导致内存泄露,不适用于业务开发。

  • coroutineScope

创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。它是一个挂起函数,需要运行在协程内或挂起函数内,为并行分解工作而设计的。

  • supervisorScope

与coroutineScope类似,不同的是子协程的异常不会影响父协程,也不会影响其他子协程。

  • MainScope

为UI组件创建主作用域。一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main,说明它是一个在主线程执行的协程作用域。推荐使用。

Android官方对协程的支持是非常友好的,KTX为Jetpack的Lifecycle相关组件提供了已经绑定UV声明周期的作用域供我们直接使用:

  • lifecycleScope

与Lifecycle绑定生命周期,生命周期被销毁时,此作用域将被取消不会造成协程泄漏,推荐使用。

  • viewModelScope

与lifecycleScope类似,与ViewModel绑定生命周期,当ViewModel被清除时,这个作用域将被取消,推荐使用。

2.2 调度器

调度器的作用是将协程限制在特定的线程执行。主要的调度器类型有:

  • Dispatchers.Main:指定执行的线程是主线程
  • Dispatchers.IO:指定执行的线程是IO线程
  • Dispatchers.Default:默认的调度器,适合执行CPU密集性的任务
  • Dispatchers.Unconfined:非限制的调度器,指定的线程可能会随着挂起的函数的发生变化

2.3 构建器

kotlinx.continues库提供的三个基本协程构建器:

  • Launch
  • async
  • runBlocking

launch{}是最常用的协程构建器,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器。

async创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用,并返回Deffer对象。可通过调用Deffer.await()方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。

runBlocking是创建一个新的协程同时阻塞当前线程,直到协程结束,主要是为测试设计。

Part 03

协程挂起、恢复原理剖析 

协程的概念最核心的点就是挂起,即函数或者某段程序可以在某个时刻暂停执行并稍后恢复。suspend是Kotlin协程最核心的关键字,使用suspend关键字修饰的函数叫作挂起函数,挂起函数只能在协程体内或者在其他挂起函数内调用。内部实现使用了Kotlin编译器的一些编译技术,被关键字suspend修饰的方法在编译阶段,编译器会修改方法的签名. 包括返回值,修饰符,入参,方法体实现。我们以下面一个简单的挂起方法来剖析。

图片

图7 挂起函数

通过AS的工具栏中 Tools->Kotlin->show Kotlin ByteCode,得到java字节码,再点击Decompile按钮反编译成java源码:

图片

图8 挂起函数反编译java源码

上面主要步骤为:

1️⃣函数返回值变成Object,函数入参编译后增加了Continuation参数。

2️⃣创建一个ContinuationImpl ,复写invokeSuspend()方法,在这个方法里面它又调用了一次自己,并且把continuation传递进去。

3️⃣在switch状态机中,label初始值为0,第一次会进入case 0分支,delay()是一个挂起函数,传入上面的continuation参数,会有一个Object类型的返回值。

4️⃣DelayKt.delay(2000, continuation)的返回结果如果是 COROUTINE_SUSPENDED,则直接return,那么方法执行就被结束了,方法就被挂起了。

这就是挂起的真正原理。协程的挂起本质上是方法的挂起,而方法的挂起本质上是return,协程的恢复本质上方法的恢复,而恢复的本质是callback回调。

Part 04

 总结 

异步编程是现代软件开发的重要组成部分,它允许我们创建响应迅速、可扩展的应用程序。Kotlin协程是一款轻量级、高效、易于使用的并发框架,借助Kotlin的语言优势,用同步的方式写出异步的代码,变得更加可维护和可读,有助于改善开发体验。在Android客户端开发中,结合Jetpack可以更加轻松使用不阻塞UI线程同时避免内存泄露。

责任编辑:庞桂玉 来源: 移动Labs
相关推荐

2023-07-23 18:47:59

Docker开源

2023-09-17 17:51:43

Android 14

2023-07-12 16:03:37

Android开发架构

2023-04-15 20:25:23

微前端

2023-07-16 18:49:42

HTTP网络

2023-08-06 07:05:25

Android优化

2023-09-12 07:10:13

Nacos架构

2023-07-02 16:09:57

人工智能人脸识别

2023-07-02 16:34:06

GPU虚拟化深度学习

2023-08-06 07:00:59

Openstack网络

2023-08-15 14:46:03

2020-12-17 10:00:16

Python协程线程

2023-07-31 08:55:15

AI技术网络暴力

2023-07-12 15:50:29

机器学习人工智能

2023-08-29 06:50:01

Javamaven

2023-10-24 19:37:34

协程Java

2022-03-18 14:38:09

隐私计算数据孤岛安全

2023-08-07 06:31:56

Kafka

2023-09-18 07:10:48

限流算法

2023-09-03 19:06:42

点赞
收藏

51CTO技术栈公众号