Android Kotlin 协程初探 | 京东物流技术团队
1 它是什么(协程 和 Kotlin协程)
1.1 协程是什么
维基百科:协程,英文Coroutine [kəru’tin] (可入厅),是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。
作为Google钦定的Android开发首选语言Kotlin,协程并不是 Kotlin 提出来的新概念,目前有协程概念的编程语言有Lua语言、Python语言、Go语言、C语言等,它只是一种编程思想,不局限于特定的语言。
而每一种编程语言中的协程的概念及实现又不完全一样,本次分享主要讲Kotlin协程。
1.2 Kotlin协程是什么
Kotlin官网:协程是轻量级线程
可简单理解:一个线程框架,是全新的处理并发的方式,也是Android上方便简化异步执行代码的方式
类似于 Java:线程池 Android:Handler和AsyncTask,RxJava的Schedulers
注:Kotlin不仅仅是面向JVM平台的,还有JS/Native,如果用kotlin来写前端,那Koltin的协程就是JS意义上的协程。如果仅仅JVM 平台,那确实应该是线程框架。
1.3 进程、线程、协程比较
可通过以下两张图理解三者的不同和关系
2 为什么选择它(协程解决什么问题)
异步场景举例:
- 第一步:接口获取当前用户token及用户信息
- 第二步:将用户的昵称展示界面上
- 第三步:然后再通过这个token获取当前用户的消息未读数
- 第四步:并展示在界面上
2.1 现有方案实现
apiService.getUserInfo().enqueue(object :Callback<User>{
override fun onResponse(call: Call<User>, response: Response<User>) {
val user = response.body()
tvNickName.text = user?.nickName
apiService.getUnReadMsgCount(user?.token).enqueue(object :Callback<Int>{
override fun onResponse(call: Call<Int>, response: Response<Int>) {
val tvUnReadMsgCount = response.body()
tvMsgCount.text = tvUnReadMsgCount.toString()
}
})
}
})
现有方案如何拿到异步任务的数据,得不到就毁掉哈哈哈,就是通过回调函数来解决。
若嵌套多了,这种画风是不是有点回调地狱的感觉,俗称的「callback hell」
2.2 协程实现
mainScope.launch {
val user = apiService.getUserInfoSuspend() //IO线程请求数据
tvNickName.text = user?.nickName //UI线程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //IO线程请求数据
tvMsgCount.text = unReadMsgCount.toString() //UI线程更新界面
}
suspend fun getUserInfoSuspend() :User? {
return withContext(Dispatchers.IO){
//模拟网络请求耗时操作
delay(10)
User("asd123", "userName", "nickName")
}
}
suspend fun getUnReadMsgCountSuspend(token:String?) :Int{
return withContext(Dispatchers.IO){
//模拟网络请求耗时操作
delay(10)
10
}
}
红色框框内的就是一个协程代码块。
可以看得出在协程实现中告别了callback,所以再也不会出现回调地狱这种情况了,协程解决了回调地狱
协程可以让我们用同步的代码写出异步的效果,这也是协程最大的优势,异步代码同步去写。
小结:协程可以异步代码同步去写,解决回调地狱,让程序员更方便地处理异步业务,更方便地切线程,保证主线程安全。
它是怎么做到的?
3 它是怎么工作的(协程的原理浅析)
3.1 协程的挂起和恢复
挂起(非阻塞式挂起)
suspend 关键字,它是协程中核心的关键字,是挂起的标识。
下面看一下上述示例代码切换线程的过程:
每一次从主线程切到IO线程都是一次协程的挂起操作;
每一次从IO线程切换主线程都是一次协程的恢复操作;
挂起和恢复是suspend函数特有的能力,其他函数不具备,挂起的内容是协程,不是挂起线程,也不是挂起函数,当线程执行到suspend函数的地方,不会继续执行当前协程的代码了,所以它不会阻塞线程,是非阻塞式挂起。
有挂起必然有恢复流程, 恢复是指将已经被挂起的目标协程从挂起之处开始恢复执行。在协程中,挂起和恢复都不需要我们手动处理,这些都是kotlin协程帮我们自动完成的。
那Kotlin协程是如何帮我们自动实现挂起和恢复操作的呢?
它是通过Continuation来实现的。 [kənˌtɪnjuˈeɪʃ(ə)n] (继续;延续;连续性;后续部分)
3.2 协程的挂起和恢复的工作原理(Continuation)
CPS + 状态机
Java中没有suspend函数,suspend是Kotlin中特有的关键字,当编译时,Kotlin编译器会将含有suspend关键字的函数进行一次转换。
这种被编译器转换在kotlin中叫CPS转换(cotinuation-passing-style)。
转换流程如下所示
程序员写的挂起函数代码:
suspend fun getUserInfo() : User {
val user = User("asd123", "userName", "nickName")
return user
}
假想的一种中间态代码(便于理解):
fun getUserInfo(callback: Callback<User>): Any? {
val user = User("asd123", "userName", "nickName")
callback.onSuccess(user)
return Unit
}
转换后的代码:
fun getUserInfo(cont: Continuation<User>): Any? {
val user = User("asd123", "userName", "nickName")
cont.resume(user)
return Unit
}
我们通过Kotlin生成字节码工具查看字节码,然后将其反编译成Java代码:
@Nullable
public final Object getUserInfo(@NotNull Continuation $completion) {
User user = new User("asd123", "userName", "nickName");
return user;
}
这也验证了确实是会通过引入一个Continuation对象来实现恢复的流程,这里的这个Continuation对象中包含了Callback的形态。
它有两个作用:1. 暂停并记住执行点位;2. 记住函数暂停时刻的局部变量上下文。
所以为什么我们可以用同步的方式写异步代码,是因为Continuation帮我们做了回调的流程。
下面看一下这个Continuation 的源码部分
可以看到这个Continuation中封装了一个resumeWith的方法,这个方法就是恢复用的。
internal abstract class BaseContinuationImpl() : Continuation<Any?> {
public final override fun resumeWith(result: Result<Any?>) {
//省略好多代码
invokeSuspend()
//省略好多代码
}
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
}
internal abstract class ContinuationImpl(
completion: Continuation<Any?>?,
private val _context: CoroutineContext?
) : BaseContinuationImpl(completion) {
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
//invokeSuspend() 这个方法是恢复的关键一步
继续看上述例子:
这是一个CPS之前的代码:
suspend fun testCoroutine() {
val user = apiService.getUserInfoSuspend() //挂起函数 IO线程
tvNickName.text = user?.nickName //UI线程更新界面
val unReadMsgCount = apiService.getUnReadMsgCountSuspend(user?.token) //挂起函数 IO线程
tvMsgCount.text = unReadMsgCount.toString() //UI线程更新界面
}
当前挂起函数里有两个挂起函数
通过kotlin编译器编译后:
fun testCoroutine(completion: Continuation<Any?>): Any? {
// TestContinuation本质上是匿名内部类
class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示协程状态机当前的状态
var label: Int = 0
// 两个变量,对应原函数的2个变量
lateinit var user: Any
lateinit var unReadMsgCount: Int
// result 接收协程的运行结果
var result = continuation.result
// suspendReturn 接收挂起函数的返回值
var suspendReturn: Any? = null
// CoroutineSingletons 是个枚举类
// COROUTINE_SUSPENDED 代表当前函数被挂起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED
// invokeSuspend 是协程的关键
// 它最终会调用 testCoroutine(this) 开启协程状态机
// 状态机相关代码就是后面的 when 语句
// 协程的本质,可以说就是 CPS + 状态机
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
}
// ...
val continuation = if (completion is TestContinuation) {
completion
} else {
// 作为参数
// ↓
TestContinuation(completion)
loop = true
while(loop) {
when (continuation.label) {
0 -> {
// 检测异常
throwOnFailure(result)
// 将 label 置为 1,准备进入下一次状态
continuation.label = 1
// 执行 getUserInfoSuspend(第一个挂起函数)
suspendReturn = getUserInfoSuspend(continuation)
// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}
1 -> {
throwOnFailure(result)
// 获取 user 值
user = result as Any
// 准备进入下一个状态
continuation.label = 2
// 执行 getUnReadMsgCountSuspend
suspendReturn = getUnReadMsgCountSuspend(user.token, continuation)
// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}
2 -> {
throwOnFailure(result)
user = continuation.mUser as Any
unReadMsgCount = continuation.unReadMsgCount as Int
loop = false
}
}
通过一个label标签控制分支代码执行,label为0,首先会进入第一个分支,首先将label设置为下一个分支的数值,然后执行第一个suspend方法并传递当前Continuation,得到返回值,如果是COROUTINE SUSPENDED,协程框架就直接return,协程挂起,当第一个suspend方法执行完成,会回调Continuation的invokeSuspend方法,进入第二个分支执行,以此类推执行完所有suspend方法。
每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复只是跳转到下一种状态中。挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的。
小结:协程的挂起和恢复的本质是CPS + 状态机
4 总结
总结几个不用协程实现起来很麻烦的骚操作:
- 如果有一个函数,它的返回值需要等到多个耗时的异步任务都执行完毕返回之后,组合所有任务的返回值作为 最终返回值
- 如果有一个函数,需要顺序执行多个网络请求,并且后一个请求依赖前一个请求的执行结果
- 当前正在执行一项异步任务,但是你突然不想要它执行了,随时可以取消
- 如果你想让一个任务最多执行3秒,超过3秒则自动取消
Kotlin协程之所以被认为是假协程,是因为它并不在同一个线程运行,而是真的会创建多个线程。
Kotlin协程在Android上只是一个类似线程池的封装,真就是一个线程框架。但是它却可以让我们用同步的代码风格写出异步的效果,至于怎么做的,这个不需要我们操心,这些都是kotlin帮我们处理好了,我们需要关心的是怎么用好它
它就是一个线程框架。
作者:京东物流 王斌
来源:京东云开发者社区 自猿其说Tech 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
IPSec VPN原理介绍 | 京东物流技术团队
背景: 什么是VPN?他是干什么用的?有什么优势?解决我们什么问题? 1 VPN的概念 VPN定义 Virtual Private Network,中文名虚拟专用网络,意思是在公用网络上仿真建立一条点到点的专用网络,进行加密通讯,解决远程访问(个人和分支机构到总部)的问题。 要理解VPN,我们需要先弄了解一个概念——隧道协议,其实质是用一种协议来传输另一种协议,其基本功能是封装和加密。我们给大家列举几个隧道协议:GRE、IPSec、SSL/TLS、VPN(WebVPN)、PPTP、L2TP。 VPN解决的问题 VPN是企业分支机构、末端网络以及个人通告公共网络访问内部私网的一个解决方案。公网上存在的问题既是VPN需要面对和解决的问题。 广域网存在的隐患: 网上传输的数据有被窃听的风险; 网上传输的数据有被篡改的风险; 通信双方可能被冒充; VPN如何保护网络实体间的通信: 通过加密技术防止数据被窃听——数据的私密性; 通过哈希技术防止数据被篡改——数据的完整性; 通过认证机制确认身份,防止数据被截获、重传——数据的源认知; 通过增加序列号机制,防止数据的重放攻击——防重放攻击; VPN...
-
下一篇
用户案例 | 珍岛集团基于 Apache DolphinScheduler 打造智能营销云平台
珍岛集团致力于打造全球领先的智能营销云平台,在国内率先推出的Marketingforce(营销力)平台,专注于人工智能、大数据、云计算在数字营销及企业数字化智能化领域的创新与实践,面向全球企业提供营销力软件及服务,以一站式智能营销生态助力企业进行数字化转型。 之前,珍岛集团使用完全开源的Apache DolphinScheduler任务调度框架,随着业务的发展,以及数据集成平台和GMA,算法计算平台越来越多的业务需求,开源版本的Apache DolphinScheduler已经不能完全满足需求,迫切地需要对Apache DolphinScheduler做一些定制化的开发。以下是珍岛集团团队最近一年在开源版本的基础上进行的优化和改进。 业务需求 技术方面 1.期待简单易用,低代码的方式; 2.Plug-in足够多,能够符合各业务模块需求; 3.活跃的开源社区,优秀的人才; 4.技术栈能够和珍岛现有各业务模块高度吻合; 5.后期新建业务模块时,不需要过多的二次开发。 业务方面 对调度系统的稳定性要求高; 高并发情况下,任务能够正常执行。 拿一个简单的业务来举例,当用户通过配置设置好受众的特...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2全家桶,快速入门学习开发网站教程
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- MySQL8.0.19开启GTID主从同步CentOS8
- MySQL数据库在高并发下的优化方案
- CentOS8编译安装MySQL8.0.19