1. 前言
这是新年以来的第一篇更文,在此给大家拜个晚年,祝大家在新的一年所想的都能如愿,同时感谢大家一直以来的支持和帮助。这篇文章其实在春节前就已经构思完了,本想着在留京过年期间写完,由于计划变更,回老家过年去了,春节期间大部分时间在走亲戚,文章也就搁置下来了。
闲话少叙,本文我将尝试给大家讲明白,Kotlin协程如何实现用同步的方式实现异步调用。相信不少同学都能说出以下几种概念中的一个或者多个。
- Kotlin suspend关键字
- Kotlin内部的Continuation机制
- Continuation Passing Style (CPS)机制
- 有限状态机机制
如果你看过Kotlin协程架构师的Deep dive into Coroutines on JVM演讲视频,相信你对上面的概念不会陌生。
视频链接👉www.youtube.com/watch?v=Yrr…
但是如果你没有深入源码探究其实现原理,相信你对上面的概念也是一知半解。本文将带领大家更加详细地了解这一知识。
2. 一个循序渐进的例子
假设有这样一个简单的场景,在App上发起一个请求,10s后拿到响应,更新到用户界面上。我们可能会遇到以下几种写法。
2.1 直接在主线程调用
况,大家都很清楚,对于客户端开发,在主线程执行耗时操作这是万万不可的。为了不阻塞主线程,我们需要通过开启新线程,使用回调的方式,将结果回传过来。
2.2 使用callback
这种方式,虽然解决了在主线程做耗时操作的问题,但是引入了新问题,在子线程更新UI,会导致应用崩溃。为了解决这个问题,我们需要通过主线程Handler,把callback post到主线程运行。
2.3 使用callback和handler组合
目前为止,我们通过线程,回调,Handler组合方式,完美地实现了执行一个异步请求,并将结果更新到UI上。
在这个方案中有三个要素:
1. 线程或者线程池技术
2. 回调机制,将结果反馈给调用者
3. 回调操作在哪个线程中执行
那么此处应该敲黑板划重点了
既然本文是讲协程,那么协程中哪些类分别对应这三个要素呢?
- Dispatchers.Main、Dispatchers.IO等对应线程
- Continuation对应线程中的回调
- DispatchedContinuation同时指定了Continuation回调,又指定了回调在哪个线程中执行。
在此先不展开讲解Continuation和DispatchedContinuation的具体细节,此处做个铺垫,后文会详细讲解。
2.4 使用协程实现
该代码是协程实现的正确代码,没有主线程执行耗时操作问题,没有子线程更新UI问题。当然也没有显式的callback,和线程切换的代码。只对suspendHeavyWork方法增加了suspend关键字,就能将异步的调用 用同步的代码实现。那么魔法在哪呢?
3. 魔法揭秘
首先贴一下完整版代码
使用Android Studio的Show Kotlin Bytecode功能查看反编译后的文件
感慨x1
看了反编译后的代码,可能需要感慨一下,哪有什么岁月静好,只不过有人在替你负重前行。用协程写代码爽,是因为编译器在后面做了不少工作,理解这背后的工作,对我们使用协程大有裨益。
困惑x1
那么编译器生成的这些代码,看起来既熟悉又陌生,说熟悉是因为就语法而言每行代码都认识(switch case都很熟悉),说陌生是因为总体而言,不太明白他们干了什么,甚至有的函数(比如invokeSuspend)连在哪调用的都不知道。
困惑x2
更让人困惑的是,状态机代码中明明出现了return语句,请问后续的代码是如何执行的?要搞清楚状态机的原理必须搞清楚这个问题。
4. 研究launch反编译
上图标出了三个重要的地方。
- lambda表达式被转换成了Function2实例,那么Function2是什么?为什么又给Function2传了一个类型为Continuation的null对象?
- invokeSuspend方法是干嘛的?注意参数是非空的。
- 调用heavyWork(this)传入了this对象。从前文我们可以看到,heavyWork方法反编译之后变成了heavyWork(Continuation var)。那么说明Function2是Continuation类型。
有了以上几个问题,那么离揭晓答案也就不远了。
4.1 invokeSuspend
首先回答最简单的那个问题,invokeSuspend方法是干嘛的。它定义在BaseContinuationImpl类中,是一个抽象方法。
从BaseContinuationImpl(public val completion:Continuation<Any?>?)我们可以知道协程中的回调是用链表串起来的。
假设有suspend函数调用如下,那么Continuation关系图如下。
- 首先一个while循环,它保证了状态机能够轮询。
- val completion = completion!! 如果当前Continuation 左边没有回调了,快速返回
- val outcome = invokeSuspend(param) 调用当前Continuation的 invokeSuspend方法
- 如果outcome === COROUTINE_SUSPENDED直接返回。这就是为什么delay方法不会阻塞当前线程的原因,遇到suspend方法 label会+1,当前Continuation会传递给delay。
- if (completion is BaseContinuationImpl)继续递归调用invokeSuspend
- 否则调用completion.resumeWith(outcome)并且返回
这段代码就是保证状态机能够运行的核心。
4.2 Function2是什么?
Function2是SuspendLambda,定义在ContinuationImpl.kt中。它是BaseContinuationImpl的子类。
5. DispatchedContinuation
internal class DispatchedContinuation<in T>( @JvmField val dispatcher: CoroutineDispatcher, @JvmField val continuation: Continuation<T> )
DispatchedContinuation 有dispatcher和continuation两个成员变量。表示在dispatcher所对应的线程,执行continuation回调。在发生线程切换时,一定会生成DispatchedContinuation对象,否则,切完线程后,就无法再切回来了。
例如 delay和withContext,切换线程后都会创建DispatchedContinuation,以记录回调要在哪个线程调用。