当前位置: 首页 > news >正文

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 转载请注明来源

相关文章:

Android Kotlin 协程初探 | 京东物流技术团队

1 它是什么&#xff08;协程 和 Kotlin协程&#xff09; 1.1 协程是什么 维基百科&#xff1a;协程&#xff0c;英文Coroutine [kəru’tin] &#xff08;可入厅&#xff09;&#xff0c;是计算机程序的一类组件&#xff0c;推广了协作式多任务的子程序&#xff0c;允许执行被…...

MySQL-DQL【数据查询语言】(图码结合)

作者:chlorine 专栏:数据库_chlorine5的博客-CSDN博客 MySQL——DDL&#xff1a;DDL ——数据定义语言 MySQL——DML&#xff1a;DML——数据操作语言 目录 一.DQL的定义 二.DQL—语法 三.DQL—基础查询(SELECT.. FROM) &#x1f449;查询多个字段 &#x1f449;设置别名 …...

HTTP 响应状态码介绍

HTTP 响应状态码用来表明特定 HTTP 请求是否成功完成。响应被归为以下五大类&#xff1a; 1xx 信息响应 (100–199) 2xx 成功响应 (200–299) 3xx 重定向消息 (300–399) 4xx 客户端错误响应 (400–499) 5xx 服务端错误响应 (500–599)1xx 信息响应 100 Continue 这个临时响应…...

vtk 绘制等高线

使用 VTK 也可以实现等高线绘制&#xff0c;在地图、气象、地质等领域中&#xff0c;经常需要将数据以多个等高线的形式进行展示。在VTK(Visualization Tookt) 中&#xff0c;有专门的AP何以支持简便地实现这一功能。 我们还可以根据算法操作的数据类型或生成的数据类型对算法…...

N-129基于springboot,vue学生宿舍管理系统

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 系统分前后台&#xff0c;项目采用前后端分离 前端技术&#xff1a;vuevue-element-admin 服务端技术&#xff1a;springboot,mybatis…...

力扣每日一题70:爬楼梯

题目描述&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1. 1 阶 1 阶 …...

分布式合集

1.Spring 的事务 Spring框架为应用程序提供了强大的事务管理功能。它通过将事务逻辑与业务逻辑分离&#xff0c;使得开发者可以专注于业务逻辑的实现&#xff0c;而不必过多关注事务的管理。Spring事务的核心是基于AOP&#xff08;面向切面编程&#xff09;的声明式事务管理&a…...

MySQL比较两个表数据的差异

一、几种比较方式 内连接&#xff08;INNER JOIN&#xff09;&#xff1a;连接两个表的相同记录&#xff0c;通过比较连接后的结果集&#xff0c;找出相同和不同的数据。外连接&#xff08;LEFT JOIN或RIGHT JOIN&#xff09;&#xff1a;连接两个表的所有记录&#xff0c;包括…...

hive使用中的参数优化与问题排查

1.使用hive的虚拟列排查错误案例 set hive.exec.rowoffsettrue; SELECT –输入文件名 INPUT__FILE__NAME, –文件中的块内偏移量 BLOCK__OFFSET__INSIDE__FILE, –文件行偏移量 ROW__OFFSET__INSIDE__BLOCK, * from hdp_lbg_zhaopin_defaultdb.zzdetail where dt‘20201117’…...

Leetcode—66.加一【简单】

2023每日刷题&#xff08;十一&#xff09; Leetcode—66.加一 实现代码1 /*** Note: The returned array must be malloced, assume caller calls free().*/ int* plusOne(int* digits, int digitsSize, int* returnSize){int num 0;int i 0;int arr[110] {0};// 进位标识…...

Babylonjs学习笔记(六)——贴图的使用

书接上回&#xff0c;这里讨论贴图的运用&#xff01;&#xff01;&#xff01; // 创建球网格const ball MeshBuilder.CreateSphere(ball,{diameter:1},scene)ball.position new Vector3(0,1,0)// 创建PRB材质const ballMat new PBRMaterial(pbr,scene)// albedoTexture 反…...

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

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

p5.js画布操作实战:创建,绑定指定元素,动态调整大小,隐藏滚动条,删除画布

文章简介 之前在 《p5.js 光速入门》 里粗略讲过一下如何使用 p5.js 创建画布。 这次要介绍几个 p5.js 提供的画布相关的方法。 创建画布时的相关配置。让画布绑定指定元素。重置画布大小。删除画布。 学习本文前你需要具备一点 p5.js 的知识&#xff0c;想了解的请查看 《p…...

vue手动拖入和导入excel模版

1.列表按钮 <el-button click“importExcel(scope.row.id)” size“small” type“text”>导入excel模版 2.按钮弹框 3.data定义数据 data () { return { projectId: ‘’, importDialogVisible: false, fileList: [], //手动上传 upload_file: ‘’, //导入excel模版…...

Linux下导出dump文件(Oracle和PG数据)

dump文件可以快速的导入导出&#xff0c;所以在数据量较大的情况下用其他方法导出数据都不如dump。 不管是什么数据库&#xff0c;第一步都需要登录Oracle用户 su - oracle登录之后可以选择导出文件到当前目录 Oracle数据库导出指定表&#xff1a; exp 数据库用户名/密码lo…...

TSINGSEE青犀睡岗离岗检测算法——确保加油站安全运营

众所周知&#xff0c;加油站是一个需要24小时营业的场所&#xff0c;由于夜间加油人员较少&#xff0c;员工极易处于疲劳或者睡眠状态&#xff0c;为保障安全和效率&#xff0c;通过TSINGSEE青犀睡岗离岗检测算法在加油站场景中&#xff0c;可以及时发现工作人员的疲劳状况&…...

gd32部分映射1/2,完全映射,备用功能选择等

一、重映射与部分映射问题 参考相应用户手册&#xff1b; 打开&#xff1a;I/O 重映射功能和调试配置&#xff1b; AFIO 端口配置寄存器 0&#xff08;AFIO_PCF0&#xff09;&#xff1b; AFIO 端口配置寄存器 1&#xff08;AFIO_PCF1&#xff09;&#xff1b; 【e.g】以定时器…...

如何高效自学(黑客技术)方法——网络安全

如果你想自学网络安全&#xff0c;首先你必须了解什么是网络安全&#xff01;&#xff0c;什么是黑客&#xff01;&#xff01; 1.无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面性&#xff0c;例如 Web 安全技术&#xff0c;既有 Web 渗透2.也有 Web 防…...

K8S基础架构租赁(Lease )

分布式系统通常需要租约(leases)&#xff0c;租约提供了锁定共享资源和在一组成员之间协调活动的机制。 在Kubernetes中&#xff0c;租约的概念由协调(k8s.io) API组中的Lease对象表示。 在Kubernetes中&#xff0c;Lease对象用于协调集群中的节点和组件之间的通信和协作。例如…...

vue使用smooth-signature实现移动端电子签字,包括横竖屏

vue使用smooth-signature实现移动端电子签字&#xff0c;包括横竖屏 1.使用smooth-signature npm install --save smooth-signature二.页面引入插件 import SmoothSignature from "smooth-signature";三.实现效果 四.完整代码 <template><div class&quo…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

C++初阶-list的底层

目录 1.std::list实现的所有代码 2.list的简单介绍 2.1实现list的类 2.2_list_iterator的实现 2.2.1_list_iterator实现的原因和好处 2.2.2_list_iterator实现 2.3_list_node的实现 2.3.1. 避免递归的模板依赖 2.3.2. 内存布局一致性 2.3.3. 类型安全的替代方案 2.3.…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)

2025年能源电力系统与流体力学国际会议&#xff08;EPSFD 2025&#xff09;将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会&#xff0c;EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility

Cilium动手实验室: 精通之旅---20.Isovalent Enterprise for Cilium: Zero Trust Visibility 1. 实验室环境1.1 实验室环境1.2 小测试 2. The Endor System2.1 部署应用2.2 检查现有策略 3. Cilium 策略实体3.1 创建 allow-all 网络策略3.2 在 Hubble CLI 中验证网络策略源3.3 …...

渲染学进阶内容——模型

最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

跨链模式:多链互操作架构与性能扩展方案

跨链模式&#xff1a;多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈&#xff1a;模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展&#xff08;H2Cross架构&#xff09;&#xff1a; 适配层&#xf…...