看,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 在不同操作系统中的具体实现会有差别,“一对一”是主流
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
果低效漫长且无助**。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-ooEWfiSg-1715676652216)]
[外链图片转存中…(img-q9SiDoRx-1715676652218)]
[外链图片转存中…(img-T2jHjmet-1715676652219)]
[外链图片转存中…(img-XunaHEVk-1715676652220)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!