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线程同时避免内存泄露。