• 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{
        override fun onResponse(call: Call, response: Response) {
            val user = response.body()
            tvNickName.text = user?.nickName
            apiService.getUnReadMsgCount(user?.token).enqueue(object :Callback{
                override fun onResponse(call: Call, response: Response) {
                    val tvUnReadMsgCount = response.body()
                    tvMsgCount.text = tvUnReadMsgCount.toString()
                }
            })
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    现有方案如何拿到异步任务的数据,得不到就毁掉哈哈哈,就是通过回调函数来解决。
    若嵌套多了,这种画风是不是有点回调地狱的感觉,俗称的「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线程更新界面
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    红色框框内的就是一个协程代码块。

    可以看得出在协程实现中告别了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
    }
    
    • 1
    • 2
    • 3
    • 4

    假想的一种中间态代码(便于理解):

    fun getUserInfo(callback: Callback): Any? {
        val user = User("asd123", "userName", "nickName")
        callback.onSuccess(user)
        return Unit
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    转换后的代码:

    fun getUserInfo(cont: Continuation): Any? {
        val user = User("asd123", "userName", "nickName")
        cont.resume(user)
        return Unit
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们通过Kotlin生成字节码工具查看字节码,然后将其反编译成Java代码:

    @Nullable
    public final Object getUserInfo(@NotNull Continuation $completion) {
       User user = new User("asd123", "userName", "nickName");
       return user;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这也验证了确实是会通过引入一个Continuation对象来实现恢复的流程,这里的这个Continuation对象中包含了Callback的形态

    它有两个作用:1. 暂停并记住执行点位;2. 记住函数暂停时刻的局部变量上下文。

    所以为什么我们可以用同步的方式写异步代码,是因为Continuation帮我们做了回调的流程。

    下面看一下这个Continuation 的源码部分

    可以看到这个Continuation中封装了一个resumeWith的方法,这个方法就是恢复用的。

    internal abstract class BaseContinuationImpl() : Continuation {
    
    
        public final override fun resumeWith(result: Result) {
            //省略好多代码
            invokeSuspend()
            //省略好多代码
        }
    
    
        protected abstract fun invokeSuspend(result: Result): Any?
    }
    
    
    internal abstract class ContinuationImpl(
        completion: Continuation?,
        private val _context: CoroutineContext?
    ) : BaseContinuationImpl(completion) {
    
    protected abstract fun invokeSuspend(result: Result): Any?
    
    //invokeSuspend() 这个方法是恢复的关键一步
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    继续看上述例子:

    这是一个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线程更新界面
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当前挂起函数里有两个挂起函数

    通过kotlin编译器编译后:

    fun testCoroutine(completion: Continuation): Any? {
        // TestContinuation本质上是匿名内部类
        class TestContinuation(completion: Continuation?) : 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? {
                result = _result
                label = label or Int.Companion.MIN_VALUE
                return testCoroutine(this)
            }
        }
    
    
        // ...
        val continuation = if (completion is TestContinuation) {
            completion
        } else {
            //                作为参数
            //                   ↓
            TestContinuation(completion)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    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
    }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    通过一个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 转载请注明来源

  • 相关阅读:
    老卫带你学---leetcode刷题(39. 组合总和)
    2.3 IOC之于注解管理bean
    adb shell 获取手机分辨率
    C语言堆栈计算器实现,中缀转后缀表达式运算过程
    Kotlin协程-try-catch基础
    Windows:Arm,我们不合适
    深度支持赛事宣发,DF平台助推第三届全国人工智能大赛顺利举办!
    CMake教程-第 5 步:安装和测试
    分布式训练 最小化部署docker swarm + docker-compose落地方案
    SpringBoot实用开发篇复习4(实用开发篇完)
  • 原文地址:https://blog.csdn.net/JDDTechTalk/article/details/134012858