Kotlin Jetpack 实战 _ 09

看,Kotlin 官方用 Continuation 而不用 CallBack 的原因出来了:Continuation 道出了它的实现原理。当然,为了理解挂起函数,我们用 CallBack 会更加的简明易懂。

下面用动画演示挂起函数在 CPS 转换过程中,函数签名的变化:

这个转换看着简单,其中也藏着一些细节。

函数类型的变化

上面 CPS 转换过程中,函数的类型发生了变化:suspend ()->String 变成了 (Continuation)-> Any?

这意味着,如果你在 Java 访问一个 Kotlin 挂起函数getUserInfo(),在 Java 看到 getUserInfo() 的类型会是:(Continuation)-> Object。(接收 Continuation 为参数,返回值是 Object)

在这个 CPS 转换中,suspend () 变成 (Continuation) 我们前面已经解释了,不难。那么函数的返回值为什么会从:String变成Any?

挂起函数的返回值

挂起函数经过 CPS 转换后,它的返回值有一个重要作用:标志该挂起函数有没有被挂起

这听起来有点绕:挂起函数,就是可以被挂起的函数,它还能不被挂起吗?是的,挂起函数也能不被挂起。

让我们来理清几个概念:

只要有 suspend 修饰的函数,它就是挂起函数,比如我们前面的例子:

suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return “BoyCoder”
}

当 getUserInfo() 执行到 withContext的时候,就会返回 CoroutineSingletons.COROUTINE_SUSPENDED 表示函数被挂起了。

现在问题来了,请问下面这个函数是挂起函数吗:

// suspend 修饰
// ↓
suspend fun noSuspendFriendList(user: String): String{
// 函数体跟普通函数一样
return “Tom, Jack”
}

答案:它是挂起函数。但它跟一般的挂起函数有个区别:它在执行的时候,并不会被挂起,因为它就是普通函数。当你写出这样的代码后,IDE 也会提示你,suspend 是多余的:

noSuspendFriendList() 被调用的时候,不会挂起,它会直接返回 String 类型:"no suspend"。这样的挂起函数,你可以把它看作伪挂起函数

返回类型是 Any?的原因

由于 suspend 修饰的函数,既可能返回 CoroutineSingletons.COROUTINE_SUSPENDED,也可能返回实际结果"no suspend",甚至可能返回 null,为了适配所有的可能性,CPS 转换后的函数返回值类型就只能是 Any?了。

小结

  • suspend 修饰的函数就是挂起函数
  • 挂起函数,在执行的时候并不一定都会挂起
  • 挂起函数只能在其他挂起函数中被调用
  • 挂起函数里包含其他挂起函数的时候,它才会真正被挂起

以上就是 CPS 转换过程中,函数签名的细节。

然而,这并不是 CPS 转换的全部,因为我们还不知道 Continuation 到底是什么。

6. CPS 转换

Continuation 这个单词,如果你查词典维基百科,可能会一头雾水,太抽象了。

通过我们文章的例子来理解 Continuation,会更容易一些。

首先,我们只需要把握住 Continuation 的词源 Continue 即可。Continue 是继续的意思,Continuation 则是继续下去要做的事情,接下来要做的事情

放到程序中,Continuation 则代表了,程序继续运行下去需要执行的代码,接下来要执行的代码 或者 剩下的代码

以上面的代码为例,当程序运行 getUserInfo() 的时候,它的 Continuation则是下图红框的代码:

Continuation 就是接下来要运行的代码剩余未执行的代码

理解了 Continuation,以后,CPS就容易理解了,它其实就是:将程序接下来要执行的代码进行传递的一种模式。

CPS 转换,就是将原本的同步挂起函数转换成CallBack 异步代码的过程。这个转换是编译器在背后做的,我们程序员对此无感知。

也许会有小伙伴嗤之以鼻:这么简单粗暴?三个挂起函数最终变成三个 Callback 吗?

当然不是。思想仍然是 CPS 的思想,但要比 Callback 高明很多。

接下来,我们一起看看挂起函数反编译后的代码是什么样吧。前面铺垫了这么多,全都是为了下一个部分准备的。

7. 字节码反编译

字节码反编译成 Java 这种事,我们干过很多次了。跟往常不同的是,这次我不会直接贴反编译后的代码,因为如果我直接贴出反编译后的 Java 代码,估计会吓退一大波人。协程反编译后的代码,逻辑实在是太绕了,可读性实在太差了。这样的代码,CPU 可能喜欢,但真不是人看的。

所以,为了方便大家理解,接下来我贴出的代码是我用 Kotlin 翻译后大致等价的代码,改善了可读性,抹掉了不必要的细节。如果你能把这篇文章里所有内容都弄懂,你对协程的理解也已经超过大部分人了。

进入正题,这是我们即将研究的对象,testCoroutine 反编译前的代码:

suspend fun testCoroutine() {
log(“start”)
val user = getUserInfo()
log(user)
val friendList = getFriendList(user)
log(friendList)
val feedList = getFeedList(friendList)
log(feedList)
}

反编译后,testCoroutine函数的签名变成了这样:

// 没了 suspend,多了 completion
fun testCoroutine(completion: Continuation<Any?>): Any? {}

由于其他几个函数也是挂起函数,所以它们的函数签名也会改变:

fun getUserInfo(completion: Continuation<Any?>): Any?{}
fun getFriendList(user: String, completion: Continuation<Any?>): Any?{}
fun getFeedList(friendList: String, completion: Continuation<Any?>): Any?{}

接下来我们分析 testCoroutine() 的函数体,因为它相当复杂,涉及到三个挂起函数的调用。

首先,在 testCoroutine 函数里,会多出一个 ContinuationImpl 的子类,它的是整个协程挂起函数的核心。代码里的注释非常详细,请仔细看。

fun testCoroutine(completion: Continuation<Any?>): Any? {

class TestContinuation(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示协程状态机当前的状态
var label: Int = 0
// 协程返回结果
var result: Any? = null

// 用于保存之前协程的计算结果
var mUser: Any? = null
var mFriendList: Any? = null

// invokeSuspend 是协程的关键
// 它最终会调用 testCoroutine(this) 开启协程状态机
// 状态机相关代码就是后面的 when 语句
// 协程的本质,可以说就是 CPS + 状态机
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return testCoroutine(this)
}
}
}

接下来则是要判断 testCoroutine 是不是初次运行,如果是初次运行,就要创建一个 TestContinuation 的实例对象。

// ↓
fun testCoroutine(completion: Continuation<Any?>): Any? {

val continuation = if (completion is TestContinuation) {
completion
} else {
// 作为参数
// ↓
TestContinuation(completion)
}
}

  • invokeSuspend 最终会调用 testCoroutine,然后走到这个判断语句
  • 如果是初次运行,会创建一个 TestContinuation 对象,completion 作为了参数
  • 这相当于用一个新的 Continuation 包装了旧的 Continuation
  • 如果不是初次运行,直接将 completion 赋值给 continuation
  • 这说明 continuation 在整个运行期间,只会产生一个实例,这能极大的节省内存开销(对比 CallBack)

接下来是几个变量的定义,代码里会有详细的注释:

// 三个变量,对应原函数的三个变量
lateinit var user: String
lateinit var friendList: String
lateinit var feedList: String

// result 接收协程的运行结果
var result = continuation.result

// suspendReturn 接收挂起函数的返回值
var suspendReturn: Any? = null

// CoroutineSingletons 是个枚举类
// COROUTINE_SUSPENDED 代表当前函数被挂起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED

然后就到了我们的状态机的核心逻辑了,具体看注释吧:

when (continuation.label) {
0 -> {
// 检测异常
throwOnFailure(result)

log(“start”)
// 将 label 置为 1,准备进入下一次状态
continuation.label = 1

// 执行 getUserInfo
suspendReturn = getUserInfo(continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

1 -> {
throwOnFailure(result)

// 获取 user 值
user = result as String
log(user)
// 将协程结果存到 continuation 里
continuation.mUser = user
// 准备进入下一个状态
continuation.label = 2

// 执行 getFriendList
suspendReturn = getFriendList(user, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

2 -> {
throwOnFailure(result)

user = continuation.mUser as String

// 获取 friendList 的值
friendList = result as String
log(friendList)

// 将协程结果存到 continuation 里
continuation.mUser = user
continuation.mFriendList = friendList

// 准备进入下一个状态
continuation.label = 3

// 执行 getFeedList
suspendReturn = getFeedList(friendList, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

3 -> {
throwOnFailure(result)

user = continuation.mUser as String
friendList = continuation.mFriendList as String
feedList = continuation.result as String
log(feedList)
loop = false
}
}

  • when 表达式实现了协程状态机
  • continuation.label 是状态流转的关键
  • continuation.label 改变一次,就代表协程切换了一次
  • 每次协程切换后,都会检查是否发生异常
  • testCoroutine 里的原本的代码,被拆分到状态机里各个状态中,分开执行
  • getUserInfo(continuation),getFriendList(user, continuation),getFeedList(friendList, continuation) 三个函数调用传的同一个 continuation 实例。
  • 一个函数如果被挂起了,它的返回值会是:CoroutineSingletons.COROUTINE_SUSPENDED
  • 切换协程之前,状态机会把之前的结果以成员变量的方式保存在 continuation 中。

警告:以上的代码是我用 Kotlin 写出的改良版反编译代码,协程反编译后真实的代码后面我也会放出来,请继续看。

8. 协程状态机动画演示

上面一大串文字和代码看着是不是有点晕?请看看这个动画演示,看完动画演示了,回过头再看上面的文字,你会有更多收获。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

是不是完了呢?并不,因为上面的动画仅演示了每个协程正常挂起的情况。 如果协程并没有真正挂起呢?协程状态机会怎么运行?

协程未挂起的情况

要验证也很简单,我们将其中一个挂起函数改成伪挂起函数即可。

// “伪”挂起函数
// 虽然它有 suspend 修饰,但执行的时候并不会真正挂起,因为它函数体里没有其他挂起函数
// ↓
suspend fun noSuspendFriendList(user: String): String{
return “Tom, Jack”
}

suspend fun testNoSuspend() {
log(“start”)
val user = getUserInfo()
log(user)
// 变化在这里
// ↓
val friendList = noSuspendFriendList(user)
log(friendList)
val feedList = getFeedList(friendList)
log(feedList)
}

testNoSuspend()这样的一个函数体,它的反编译后的代码逻辑怎么样的?

答案其实很简单,它的结构跟前面的testCoroutine()是一致的,只是函数名字变了而已,Kotlin 编译器 CPS 转换的逻辑只认 suspend 关键字。就算是“伪”挂起函数,Kotlin 编译器也照样会进行 CPS 转换。

when (continuation.label) {
0 -> {

}

1 -> {

// 变化在这里
// ↓
suspendReturn = noSuspendFriendList(user, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
//go to next state
}
}

2 -> {

}

3 -> {

}
}

testNoSuspend()的协程状态机是怎么运行的?

其实很容易能想到,continuation.label = 0,2,3 的情况都是不变的,唯独在 label = 1 的时候,suspendReturn == sFlag这里会有区别。

具体区别我们通过动画来看吧:

通过动画我们很清楚的看到了,对于“伪”挂起函数suspendReturn == sFlag是会走 else 分支的,在 else 分支里,协程状态机会直接进入下一个状态。

现在只剩最后一个问题了:

if (suspendReturn == sFlag) {
} else {
// 具体代码是如何实现的?
// ↓
//go to next state
}

答案其实也很简单:如果你去看协程状态机的字节码反编译后的 Java,会看到很多 label。协程状态机底层字节码是通过 label 来实现这个go to next state的。由于 Kotlin 没有类似 goto 的语法,下面我用伪代码来表示go to next state的逻辑。

// 伪代码
// Kotlin 没有这样的语法
// ↓ ↓
label: whenStart
when (continuation.label) {
0 -> {

}

1 -> {

suspendReturn = noSuspendFriendList(user, continuation)
if (suspendReturn == sFlag) {
return suspendReturn
} else {
result = suspendReturn
// 让程序跳转到 label 标记的地方
// 从而再执行一次 when 表达式
goto: whenStart
}
}

2 -> {

}

3 -> {

}
}

注意:以上是伪代码,它只是跟协程状态机字节码逻辑上等价,为了不毁掉你钻研协程的乐趣,我不打算在这里解释协程原始的字节码。我相信如果你理解了我的文章以后,再去看协程反编译的真实代码,一定会游刃有余。

下面的建议会有助于你看协程真实的字节码: 协程状态机真实的原理是:通过 label 代码段嵌套,配合 switch 巧妙构造出一个状态机结构,这种逻辑比较复杂,相对难懂一些。(毕竟 Java 的 label 在实际开发中用的很少。)

真实的协程反编译代码大概长这样:

@Nullable
public static final Object testCoroutine(@NotNull Continuation $completion) {
Object KaTeX parse error: Expected '}', got 'EOF' at end of input: …label37: { if (completion instanceof <TestSuspendKt$testCoroutine$1>) {
c o n t i n u a t i o n = ( < T e s t S u s p e n d K t continuation = (<TestSuspendKt continuation=(<TestSuspendKttestCoroutine 1 > ) 1>) 1>)completion;
if ((((<TestSuspendKt$testCoroutine 1 > ) 1>) 1>)continuation).label & Integer.MIN_VALUE) != 0) {
((<TestSuspendKt$testCoroutine 1 > ) 1>) 1>)continuation).label -= Integer.MIN_VALUE;
break label37;
}
}

c o n t i n u a t i o n = n e w C o n t i n u a t i o n I m p l ( continuation = new ContinuationImpl( continuation=newContinuationImpl(completion) {
// $FF: synthetic field
Object result;
int label;
Object L$0;
Object L$1;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestSuspendKt.testCoroutine(this);
}
};
}

Object var10000;
label31: {
String user;
String friendList;
Object var6;
label30: {
Object r e s u l t = ( ( < T e s t S u s p e n d K t result = ((<TestSuspendKt result=((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).result;
var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<TestSuspendKt$testCoroutine 1 > ) 1>) 1>)continuation).label) {
case 0:
ResultKt.throwOnFailure( r e s u l t ) ; l o g ( " s t a r t " ) ; ( ( < T e s t S u s p e n d K t result); log("start"); ((<TestSuspendKt result);log("start");((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).label = 1;
var10000 = getUserInfo((Continuation) c o n t i n u a t i o n ) ; i f ( v a r 10000 = = v a r 6 ) r e t u r n v a r 6 ; b r e a k ; c a s e 1 : R e s u l t K t . t h r o w O n F a i l u r e ( continuation); if (var10000 == var6) { return var6; } break; case 1: ResultKt.throwOnFailure( continuation);if(var10000==var6)returnvar6;break;case1:ResultKt.throwOnFailure(result);
var10000 = r e s u l t ; b r e a k ; c a s e 2 : u s e r = ( S t r i n g ) ( ( < T e s t S u s p e n d K t result; break; case 2: user = (String)((<TestSuspendKt result;break;case2:user=(String)((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).L 0 ; R e s u l t K t . t h r o w O n F a i l u r e ( 0; ResultKt.throwOnFailure( 0;ResultKt.throwOnFailure(result);
var10000 = r e s u l t ; b r e a k l a b e l 30 ; c a s e 3 : f r i e n d L i s t = ( S t r i n g ) ( ( < T e s t S u s p e n d K t result; break label30; case 3: friendList = (String)((<TestSuspendKt result;breaklabel30;case3:friendList=(String)((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).L 1 ; u s e r = ( S t r i n g ) ( ( < T e s t S u s p e n d K t 1; user = (String)((<TestSuspendKt 1;user=(String)((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).L 0 ; R e s u l t K t . t h r o w O n F a i l u r e ( 0; ResultKt.throwOnFailure( 0;ResultKt.throwOnFailure(result);
var10000 = $result;
break label31;
default:
throw new IllegalStateException(“call to ‘resume’ before ‘invoke’ with coroutine”);
}

user = (String)var10000;
log(user);
((<TestSuspendKt$testCoroutine 1 > ) 1>) 1>)continuation).L 0 = u s e r ; ( ( < T e s t S u s p e n d K t 0 = user; ((<TestSuspendKt 0=user;((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).label = 2;
var10000 = getFriendList(user, (Continuation)$continuation);
if (var10000 == var6) {
return var6;
}
}

friendList = (String)var10000;
log(friendList);
((<TestSuspendKt$testCoroutine 1 > ) 1>) 1>)continuation).L 0 = u s e r ; ( ( < T e s t S u s p e n d K t 0 = user; ((<TestSuspendKt 0=user;((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).L 1 = f r i e n d L i s t ; ( ( < T e s t S u s p e n d K t 1 = friendList; ((<TestSuspendKt 1=friendList;((<TestSuspendKttestCoroutine 1 > ) 1>) 1>)continuation).label = 3;
var10000 = getFeedList(friendList, (Continuation)$continuation);
if (var10000 == var6) {
return var6;
}
}

String feedList = (String)var10000;
log(feedList);
return Unit.INSTANCE;
}

9. 结尾

回过头看线程和协程之间的关系:

线程

  • 线程是操作系统级别的概念
  • 我们开发者通过编程语言(Thread.java)创建的线程,本质还是操作系统内核线程的映射
  • JVM 中的线程与内核线程的存在映射关系,有“一对一”,“一对多”,“M对N”。JVM 在不同操作系统中的具体实现会有差别,“一对一”是主流

文末

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-ktxl70W5-1714928531123)]

【算法合集】

[外链图片转存中…(img-tHU48TZ4-1714928531124)]

【延伸Android必备知识点】

[外链图片转存中…(img-eod3vgiW-1714928531125)]

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值