您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Android Kotlin 协程初探
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Android Kotlin 协程初探
自猿其说Tech
2023-01-05
IP归属:未知
38400浏览
# 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 进程、线程、协程比较 可通过以下两张图理解三者的不同和关系 ![](//img1.jcloudcs.com/developer.jdcloud.com/b4dac7ca-e73c-499f-83f8-f9bab96b37cd20230105143800.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/95729bb8-cd5b-45e6-92bf-d83a20cdc5bc20230105143812.png) # 2 为什么选择它(协程解决什么问题) 异步场景举例: 1. 第一步:接口获取当前用户token及用户信息 1. 第二步:将用户的昵称展示界面上 1. 第三步:然后再通过这个token获取当前用户的消息未读数 1. 第四步:并展示在界面上 ## 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 } } ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/d95c50ba-59af-4501-9b80-cd5a826f8af620230105143934.png) 红色框框内的就是一个协程代码块。 可以看得出在协程实现中告别了callback,所以再也不会出现回调地狱这种情况了,协程解决了回调地狱 ![](//img1.jcloudcs.com/developer.jdcloud.com/51dff0ff-e1b3-45ad-800b-c9b145a1f18420230105143952.png) 协程可以让我们用同步的代码写出异步的效果,这也是协程最大的优势,异步代码同步去写。 **小结:协程可以异步代码同步去写,解决回调地狱,让程序员更方便地处理异步业务,更方便地切线程,保证主线程安全。** 它是怎么做到的? # 3 它是怎么工作的(协程的原理浅析) ## 3.1 协程的挂起和恢复 **挂起(非阻塞式挂起)** suspend 关键字,它是协程中核心的关键字,是挂起的标识。 下面看一下上述示例代码切换线程的过程: ![](//img1.jcloudcs.com/developer.jdcloud.com/78475b92-bc32-437e-8518-767c5f197f8620230105144026.png) 每一次从主线程切到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代码:** ```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 的源码部分 ![](//img1.jcloudcs.com/developer.jdcloud.com/86381717-dd2e-44a6-9efc-41c609f9516a20230105144254.png) 可以看到这个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 总结 总结几个不用协程实现起来很麻烦的骚操作: 1. 如果有一个函数,它的返回值需要等到多个耗时的异步任务都执行完毕返回之后,组合所有任务的返回值作为 最终返回值 1. 如果有一个函数,需要顺序执行多个网络请求,并且后一个请求依赖前一个请求的执行结果 1. 当前正在执行一项异步任务,但是你突然不想要它执行了,随时可以取消 1. 如果你想让一个任务最多执行3秒,超过3秒则自动取消 Kotlin协程之所以被认为是假协程,是因为它并不在同一个线程运行,而是真的会创建多个线程。 Kotlin协程在Android上只是一个类似线程池的封装,真就是一个线程框架。但是它却可以让我们用同步的代码风格写出异步的效果,至于怎么做的,这个不需要我们操心,这些都是kotlin帮我们处理好了,我们需要关心的是怎么用好它 它就是一个线程框架。 ------------ 自猿其说Tech-JDL京东物流技术与数据智能部 **作者:王斌**
原创文章,需联系作者,授权转载
上一篇:关于数据库分库分表的一点想法
下一篇:交换机与路由器你了解多少?
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号