写点什么

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

  • 2023-10-24
    北京
  • 本文字数:4746 字

    阅读完需:约 16 分钟

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?.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 代码:


@Nullablepublic 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 = truewhile(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. 如果有一个函数,它的返回值需要等到多个耗时的异步任务都执行完毕返回之后,组合所有任务的返回值作为 最终返回值

  2. 如果有一个函数,需要顺序执行多个网络请求,并且后一个请求依赖前一个请求的执行结果

  3. 当前正在执行一项异步任务,但是你突然不想要它执行了,随时可以取消

  4. 如果你想让一个任务最多执行 3 秒,超过 3 秒则自动取消


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


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


它就是一个线程框架。


作者:京东物流 王斌

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

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Android Kotlin 协程初探 | 京东物流技术团队_kotlin_京东科技开发者_InfoQ写作社区