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 为什么选择它(协程解决什么问题)

异步场景举例:

  1. 第一步:接口获取当前用户token及用户信息
  2. 第二步:将用户的昵称展示界面上
  3. 第三步:然后再通过这个token获取当前用户的消息未读数
  4. 第四步:并展示在界面上

2.1 现有方案实现

apiService.getUserInfo().enqueue(object :Callback<User>{override fun onResponse(call: Call<User>, response: Response<User>) {val user = response.body()tvNickName.text = user?.nickNameapiService.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: Anylateinit 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 = _resultlabel = label or Int.Companion.MIN_VALUEreturn 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// 执行 getUnReadMsgCountSuspendsuspendReturn = getUnReadMsgCountSuspend(user.token, continuation)// 判断是否挂起if (suspendReturn == sFlag) {return suspendReturn} else {result = suspendReturn//go to next state}}2 -> {throwOnFailure(result)user = continuation.mUser as AnyunReadMsgCount = continuation.unReadMsgCount as Intloop = false
}
}

通过一个label标签控制分支代码执行,label为0,首先会进入第一个分支,首先将label设置为下一个分支的数值,然后执行第一个suspend方法并传递当前Continuation,得到返回值,如果是COROUTINE SUSPENDED,协程框架就直接return,协程挂起,当第一个suspend方法执行完成,会回调Continuation的invokeSuspend方法,进入第二个分支执行,以此类推执行完所有suspend方法。

每一个挂起点和初始挂起点对应的 Continuation 都会转化为一种状态,协程恢复只是跳转到下一种状态中。挂起函数将执行过程分为多个 Continuation 片段,并且利用状态机的方式保证各个片段是顺序执行的。

小结:协程的挂起和恢复的本质是CPS + 状态机

4 总结

总结几个不用协程实现起来很麻烦的骚操作:

  1. 如果有一个函数,它的返回值需要等到多个耗时的异步任务都执行完毕返回之后,组合所有任务的返回值作为 最终返回值
  2. 如果有一个函数,需要顺序执行多个网络请求,并且后一个请求依赖前一个请求的执行结果
  3. 当前正在执行一项异步任务,但是你突然不想要它执行了,随时可以取消
  4. 如果你想让一个任务最多执行3秒,超过3秒则自动取消

Kotlin协程之所以被认为是假协程,是因为它并不在同一个线程运行,而是真的会创建多个线程。

Kotlin协程在Android上只是一个类似线程池的封装,真就是一个线程框架。但是它却可以让我们用同步的代码风格写出异步的效果,至于怎么做的,这个不需要我们操心,这些都是kotlin帮我们处理好了,我们需要关心的是怎么用好它

它就是一个线程框架。

作者:京东物流 王斌

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/116570.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SqueezeNet 一维,二维网络复现 pytorch 小白易懂版

SqueezeNet 时隔一年我又开始复现神经网络的经典模型&#xff0c;这次主要复的是轻量级网络全家桶&#xff0c;轻量级神经网络旨在使用更小的参数量&#xff0c;无限的接近大模型的准确率&#xff0c;降低处理时间和运算量&#xff0c;这次要复现的是轻量级网络的非常经典的一…

muduo源码学习base——Atomic(原子操作与原子整数)

Atomic(原子操作与原子整数&#xff09; 前置知识AtomicIntegerTget()getAndAdd()getAndSet() 关于原子操作实现无锁队列(lock-free-queue) 前置知识 happens-before&#xff1a; 用来描述两个操作的内存可见性 如果操作 X happens-before 操作 Y&#xff0c;那么 X 的结果对于…

有奖招募——2023年度清华社“荐书官”活动今日开始了!

又到“1024程序员节”了&#xff0c;维护网络世界稳定和平的程序员大大们&#xff0c;辛苦了&#xff01;生活难免有bug&#xff0c;来给彼此个hug~ 过完1024&#xff0c;这一年也快要结束了&#xff0c;岁末回顾又要提上日程。很多人都有整理年度书单的习惯&#xff0c;那么这…

架构风格区别-架构案例(五十九)

管道-过滤器和仓库的区别&#xff1f; 独立的数据仓库&#xff0c;处理流独立&#xff0c;处理数据用连接仓库工具数据与处理在一起&#xff0c;改动的话需要重启系统需要仓库工具与仓库连接&#xff0c;数据与处理分离&#xff0c;性能差可以支持并发连接访问仓库&#xff0c…

【经验分享】如何构建openGauss开发编译提交一体化环境

前文 本文适合对openGauss源代码有好奇心的爱好者&#xff0c;那么一个友好的openGauss源代码环境应该是怎么样的。openGauss的开发环境是如何设置的&#xff1f;openGauss的编译环境是如何构建的&#xff1f;如何向openGauss提交代码&#xff0c;笔者集合官方和几位博主实践提…

2023年【危险化学品生产单位主要负责人】考试报名及危险化学品生产单位主要负责人模拟考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品生产单位主要负责人考试报名考前必练&#xff01;安全生产模拟考试一点通每个月更新危险化学品生产单位主要负责人模拟考试题题目及答案&#xff01;多做几遍&#xff0c;其实通过危险化学品生产单位主要负…

用nodejs爬虫台湾痞客邦相册

情景:是这样的,我想保存一些喜欢的小伙伴的照片,一张张保存太慢了, 所以我写了个js,放在国外服务器爬,国内的自己解决~ 使用方法 1.点相册随便一张, 复制url, 这张开始接下来的图片都会保存 /*** 2023年10月23日 22:58:44* 支持解析痞客邦相册* 只需要复制相册第一张图片的ur…

新款模块上线实现SIP模块与扩拨电话之间打点与喊话功能 IP矿用电话模块SV-2800VP

新款模块上线实现SIP模块与扩拨电话之间打点与喊话功能 IP矿用电话模块SV-2800VP 一、简介 SV-2800VP系列模块是我司设计研发的一款用于井下的矿用IP音频传输模块&#xff0c;可用此模块打造一套低延迟、高效率、高灵活和多扩展的IP矿用广播对讲系统&#xff0c;亦可对传统煤…

Linux 下安装配置部署MySql8.0

一 . 准备工作 MySQL安装包&#xff1a;在官网下载需要的版本&#xff0c;这里我用的版本是 MySQL 8.0.34 https://dev.mysql.com/downloads/mysql/ 本次linux机器使用的是阿里云ECS实例 二 . 开始部署 1. 将安装包上传至服务器 解压到当前文件夹 tar -zxvf mysql-8.0.34…

SAP HANA Time Zone设置

通常对于MINICHECK中检查出来的Timezone时区设置问题&#xff0c;可以通过以下方式进行修改 对于ABAP系统 修改HANA 参数即可 • indexserver.ini -> [global] -> timezone_default_data_client_name 000 • indexserver.ini -> [global] -> timezone_default_da…

蓝桥杯每日一题2023.10.21

后缀表达式 - 蓝桥云课 (lanqiao.cn) 题目描述 题目分析 30分解法&#xff1a;要求出最大的结果就需要加的数越大&#xff0c;减的数越小&#xff0c;以此为思路简单列举即可 #include<bits/stdc.h> using namespace std; typedef long long ll; const int N 2e5 10…

微信小程序设计之主体文件app-json-pages

一、新建一个项目 首先&#xff0c;下载微信小程序开发工具&#xff0c;具体下载方式可以参考文章《微信小程序开发者工具下载》。 然后&#xff0c;注册小程序账号&#xff0c;具体注册方法&#xff0c;可以参考文章《微信小程序个人账号申请和配置详细教程》。 在得到了测…

【2021集创赛】Digilent杯二等奖:基于FPGA的动态视觉感知融合的运动目标检测系统

杯赛题目&#xff1a;Diligent杯&#xff1a;基于FPGA开源软核的硬件加速智能平台 参赛组别&#xff1a;A组 设计任务&#xff1a; 利用业界主流软核处理器(仅限于Cortex-M系列及 RISC-V系列)在限定的DIGILENT官方FPGA平台上构建SoC片上系统&#xff0c;在 SoC中添加面向智能应…

Python数据挖掘 | 升级版自动查核酸

&#x1f4d5;作者简介&#xff1a;热爱跑步的恒川&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于恒川的日常汇报系列&#xff0c;大家有兴趣的可以看一看 &#x1f4d8;相关专栏C语言初阶、C…

GoLong的学习之路(五)语法之数组

书接上回&#xff0c;上回书说到&#xff0c;循环语句&#xff0c;在go中循环语句的少了whlie这个关键词&#xff0c;但是与之for可以改这个改这个特点。并且在终止关键词中&#xff0c;又有标签可以方便&#xff0c;停止。这次说数组 文章目录 Array(数组)数组的初始化方法一方…

计算机网络_03_tcp/ip四层模型

文章目录 1.为什么会有tcp/ip?2.tcp/ip是什么?3.为什么会有tcp/ip四层模型?4.tcp/ip四层模型介绍 1.为什么会有tcp/ip? 早期的计算机(计算机网络没有出现之前)几乎都是各自为战, 各种操作系统厂家百花齐放, 市面上的大部分计算机使用的都是不同的操作系统, 为每个人提供定…

git简介和指令

git是一个开源的的分布式版本控制系统&#xff0c;用于高效的管理各种大小项目和文件 用途&#xff1a;防止代码丢失&#xff0c;做备份 项目的版本管理和控制&#xff0c;可以通过设置节点进行跳转 建立各自的开发环境分支&#xff0c;互不影响&#xff0c;方便合并 在多终端开…

【Javascrpt】比较,逻辑运算符

目录 比较运算符 逻辑运算符 &&(与&#xff09; ||&#xff08;或&#xff09; 两真&#xff08;||左侧为真&#xff0c;||右侧为真&#xff09; 两假&#xff08;||左侧为假&#xff0c;右侧为假&#xff09; 一真一假&#xff08;||一侧为假&#xff0c;另一侧为…

需要在 MySQL 服务器中监控的重要指标

MySQL是一个开源的关系数据库管理系统&#xff0c;它基于客户端-服务器模型运行&#xff0c;使用SQL作为其通信模式。它具有灵活性和可扩展性、高安全性、易用性以及无缝处理大型数据集的能力&#xff0c;由于其广泛的功能&#xff0c;MySQL 被用作数据库管理系统的一部分。 什…

ubuntu20.04运用startup application开机自启动python程序

运用startup application开机自启动python程序。在终端中输入gnome-session-properties,如果显示没有则先进行安装&#xff0c;sudo apt-get update 和sudo apt install StartupApplications(根据显示提示安装)。在显示程序中搜索startup&#xff0c;打开应用程序。 在程序目录…