Kotlin掛起函數(shù)原理示例剖析
一、序言
Kotlin掛起函數(shù)平時在學(xué)習(xí)和工作中用的比較多,掌握其原理還是很有必要的。本文將一步一步帶著大家分析其原理實現(xiàn)。
ps: 文中所用的Kotlin版本是1.7.0。
二、CPS原理
在某個Kotlin函數(shù)的前面加個suspend函數(shù),它就成了掛起函數(shù)(雖然內(nèi)部不一定會掛起,內(nèi)部不掛起的稱為偽掛起函數(shù))。
先隨便寫個掛起函數(shù)
suspend fun getUserName(): String {
delay(1000L)
return "云天明"
}
然后通過Android Studio的Tools->Kotlin->Show Kotlin Bytecode->Decompile,現(xiàn)在我們拿到了Kotlin字節(jié)碼反編譯之后的Java代碼:
public static final Object getUserName(@NotNull Continuation var0) {
...
}
可以看到該函數(shù)被編譯之后,多了一個Continuation參數(shù),其次,返回值變成了Object。下面,我們詳細來討論一下這2種變化:函數(shù)參數(shù)和函數(shù)返回值。
CPS參數(shù)變化
上面的suspend fun getUserName(): String函數(shù),如果我在Java中調(diào)用的話,會看到Android Studio提示我們

從圖中可以看到,新增了一個參數(shù),也就是Continuation,它其實是一個Callback,只是換了個名字而已。
來看下它的定義:
/**
* Interface representing a continuation after a suspension point that returns a value of type `T`.
*/
@SinceKotlin("1.3")
public interface Continuation<in T> {
/**
* 當(dāng)前continuation所在協(xié)程的上下文
*/
public val context: CoroutineContext
/**
* 繼續(xù)執(zhí)行后面的協(xié)程代碼,同時把結(jié)果回調(diào)出去,結(jié)果可能是成功或失敗
*/
public fun resumeWith(result: Result<T>)
}
這個Callback接口會在resumeWith回調(diào)結(jié)果給外部。
CPS返回值變化
在上面的Continuation接口的定義中,其實還有個小細節(jié),它帶了個泛型T。這個泛型T就是我們suspend函數(shù)返回值的類型,上面的getUserName返回值是String,編譯之后,這個String就來到了Continuation的泛型中。
而getUserName編譯之后的返回值變成了Object。為啥是Object?它有什么用?這個返回值其實是用來標識該函數(shù)是否掛起的標志,如果返回值是Intrinsics.COROUTINE_SUSPENDED,那么說明該函數(shù)被掛起了(掛起函數(shù)的結(jié)果不是通過函數(shù)返回值來獲取的,而是通過Continuation,也就是Callback回調(diào)得到的結(jié)果)。
如果該函數(shù)是偽掛起函數(shù)(里面沒有其他掛起函數(shù),但還是會進行CPS轉(zhuǎn)換),則是直接返回結(jié)果。
舉個例子,下面這個就是真正的掛起函數(shù):
suspend fun getUserName(): String {
delay(1000L)
return "云天明"
}
當(dāng)執(zhí)行到delay的時候,就會返回Intrinsics.COROUTINE_SUSPENDED表示該函數(shù)被掛起了。
下面這個則是偽掛起函數(shù):
suspend fun getName():String {
return "程心"
}
這種偽掛起函數(shù)不會返回Intrinsics.COROUTINE_SUSPENDED,而是直接返回結(jié)果,它不會被掛起。它看起來就僅僅是一個普通函數(shù),但還是會進行CPS轉(zhuǎn)換,CPS轉(zhuǎn)換只認suspend關(guān)鍵字。你如果像上面這樣寫,其實Android Studio也會提示你,說這個suspend關(guān)鍵字沒用,叫你把它移除掉。
所以,suspend函數(shù)編譯之后的返回值變成了Object,因為要兼容偽掛起函數(shù)的返回值,而偽掛起函數(shù)可能返回任何值,而且還可能為空。
下面我們就來詳細的探索一下掛起函數(shù)的底層原理,看看掛起函數(shù)反編譯之后是什么樣子。
三、掛起函數(shù)的反編譯
我們先寫個很簡單的suspend函數(shù),然后將其反編譯,然后分析一下。具體的流程是我們用Android Studio寫個掛起函數(shù)的demo,然后編譯成apk,然后將apk用jadx反編譯一下,拿到對應(yīng)class的反編譯Java源碼,這樣弄出來的源碼我感覺比直接通過Android Studio的Tools->Kotlin->Show Kotlin拿到的源碼稍微好看懂一些。
首先,我創(chuàng)建了一個CpsTest.kt文件,然后在里面寫了一個函數(shù):
package com.xfhy.coroutine
import kotlinx.coroutines.delay
suspend fun getUserName(): String {
delay(1000L)
return "云天明"
}
就這樣,一個很普通的掛起函數(shù),在內(nèi)部只是簡單調(diào)用了下delay,延遲1000L,再返回結(jié)果“云天明”。雖然這個函數(shù)很簡單,但反編譯出來的代碼卻有點多,而且不好看懂,我先把原代碼貼出來,待會兒再放我重新組織過的代碼,作為對比:
public final class CpsTestKt {
@Nullable
public static final Object getUserName(@NotNull Continuation var0) {
Object $continuation;
label20: {
if (var0 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var0;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl(var0) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return CpsTestKt.getUserName(this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "云天明";
}
}
這反編譯之后的東西不太好看懂,我重新組織了一下:
public final class CpsTestKt {
public static final Object getUserName(Continuation<? super java.lang.String> continuation) {
//這個TestContinuation實質(zhì)上是一個匿名內(nèi)部類,這里給它取個名字而已
final class TestContinuation extends ContinuationImpl {
//協(xié)程狀態(tài)機當(dāng)前的狀態(tài)
int label;
//保存invokeSuspend回調(diào)時吐出來的返回結(jié)果
Object result;
TestContinuation(Continuation continuation) {
super(continuation);
}
//invokeSuspend比較重要,它是狀態(tài)機的入口,會將執(zhí)行流程交給getUserName再次調(diào)用
//協(xié)程的本質(zhì),就是CPS+狀態(tài)機
public final Object invokeSuspend(Object obj) {
//callback回調(diào)時會把結(jié)果帶出來
this.result = obj;
this.label |= Integer.MIN_VALUE;
//開啟協(xié)程狀態(tài)機
return CpsTestKt.getUserName(this);
}
}
TestContinuation testContinuation;
label20:
{
//不是第一次進入,則走這里,把continuation轉(zhuǎn)成TestContinuation,TestContinuation只會生成一個實例,不會每次都生成。
if (continuation instanceof TestContinuation) {
testContinuation = (TestContinuation) continuation;
if ((testContinuation.label & Integer.MIN_VALUE) != 0) {
testContinuation.label -= Integer.MIN_VALUE;
break label20;
}
}
//如果是第一次進入getUserName,則TestContinuation還沒被創(chuàng)建,會走到這里,此時先去創(chuàng)建一個TestContinuation
testContinuation = new TestContinuation(continuation);
}
//將之前執(zhí)行的結(jié)果取出來
Object $result = testContinuation.result;
//掛起的標志,如果需要掛起的話,就返回這個flag
Object flag = IntrinsicsKt.getCOROUTINE_SUSPENDED();
//狀態(tài)機
switch (testContinuation.label) {
case 0:
// 檢測異常
ResultKt.throwOnFailure($result);
//將label的狀態(tài)改成1,方便待會兒執(zhí)行delay后面的代碼
testContinuation.label = 1;
//0. 調(diào)用DelayKt.delay函數(shù)
//1. 將testContinuation傳了進去
//2. DelayKt.delay是一個掛起函數(shù),正常情況下,它會立馬返回一個值:IntrinsicsKt.COROUTINE_SUSPENDED(也就是這里的flag),表示該函數(shù)已被掛起,這里就直接return了,該函數(shù)被掛起
//3. 恢復(fù)執(zhí)行:在DelayKt.delay內(nèi)部,到了指定的時間后就會調(diào)用testContinuation這個Callback的invokeSuspend
//4. invokeSuspend中又將執(zhí)行g(shù)etUserName函數(shù),同時將之前創(chuàng)建好的testContinuation傳入其中,開始執(zhí)行后面的邏輯(label為1的邏輯),該函數(shù)繼續(xù)往后面執(zhí)行(也就是恢復(fù)執(zhí)行)
if (DelayKt.delay(1000L, testContinuation) == flag) {
return flag;
}
break;
case 1:
// 檢測異常
ResultKt.throwOnFailure($result);
//label 1這里沒有return,而是會走到下面的return "云天明"語句
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "云天明";
}
}
在getUserName函數(shù)中,會多出一個ContinuationImpl的子類,它是一個匿名內(nèi)部類(為了方便,給它取了個名字TestContinuation),也是整個協(xié)程掛起函數(shù)的核心。在這個TestContinuation中有2個變量
- label: 協(xié)程狀態(tài)機當(dāng)前的狀態(tài)
- result: 保存invokeSuspend回調(diào)時吐出來的返回結(jié)果
invokeSuspend是一個抽象方法,當(dāng)協(xié)程從掛起狀態(tài)想要恢復(fù)時,就得調(diào)用這個invokeSuspend,然后繼續(xù)走狀態(tài)機邏輯,繼續(xù)執(zhí)行后面的代碼。具體是怎么調(diào)用這個invokeSuspend的,后面有機會再細說。暫時我們只要知道,這里是恢復(fù)的入口就行。invokeSuspend內(nèi)部會把結(jié)果(這個結(jié)果可能是正常的結(jié)果,也可能是Exception)取出來,開啟協(xié)程狀態(tài)機。
分析完TestContinuation,再來看一下第一次進入getUserName是怎么走的。
- 首先,第一次進入時,continuation肯定不是TestContinuation,因為此時還沒有new過TestContinuation實例,所以會走到創(chuàng)建TestContinuation的邏輯,并且會把continuation包進去。
- 然后剛創(chuàng)建完的testContinuation的label未賦其他值,那就是初始值0了。那么switch狀態(tài)機那里,就走case 0,先把label改成1,因為馬上就要掛起了,待會兒恢復(fù)時需要執(zhí)行下一個狀態(tài)的代碼。
- 調(diào)用Kotlin的庫函數(shù)delay,它是一個掛起函數(shù),將testContinuation傳入其中,方便它進行invokeSuspend回調(diào)。調(diào)用掛起函數(shù),那么它可能會返回
COROUTINE_SUSPENDED,表示它已經(jīng)被掛起了,如果是掛起了那么getUserName就走完了,到時會從invokeSuspend恢復(fù)。在還沒有恢復(fù)的時候,這個協(xié)程所在的線程可以去做其他事情。
恢復(fù)的時候,又開始從頭走getUserName,此時的continuation已經(jīng)是TestContinuation,不會重新創(chuàng)建。它的label之前已經(jīng)被改成1了的,所以switch狀態(tài)機那里,會走到case 1,先檢測一下有沒有異常,沒有異常就返回真正的返回值了“云天明”。
分析到這里也就完了,上面就是一個非常簡單的掛起函數(shù)的反編譯分析的整個過程。下面我們簡單分析一下偽掛起函數(shù)會帶來什么效果。
四、偽掛起函數(shù)
在之前的CpsTest.kt里面簡單改一下
suspend fun fakeSuspendFun() = "維德"
suspend fun getUserName(): String {
println(fakeSuspendFun())
return "云天明"
}
像fakeSuspendFun這種就是偽掛起函數(shù),平時不建議像fakeSuspendFun這么寫,即使寫了,Android Studio也會提示你,這suspend關(guān)鍵字沒用,內(nèi)部沒有掛起。它內(nèi)部沒有掛起的邏輯,但是它有suspend關(guān)鍵字,那么Kotlin編譯器依然會給它做CPS轉(zhuǎn)換。
public final class CpsTestKt {
@Nullable
public static final Object fakeSuspendFun(@NotNull Continuation<? super java.lang.String> $completion) {
return "維德";
}
@Nullable
public static final Object getUserName(@NotNull Continuation<? super java.lang.String> continuation) {
final class TestContinuation extends ContinuationImpl {
int label;
Object result;
TestContinuation(Continuation continuation) {
super(continuation);
}
public final Object invokeSuspend(Object obj) {
this.result = obj;
this.label |= Integer.MIN_VALUE;
return CpsTestKt.getUserName(this);
}
}
TestContinuation testContinuation;
label20:
{
if (continuation instanceof TestContinuation) {
testContinuation = (TestContinuation) continuation;
if ((testContinuation.label & Integer.MIN_VALUE) != 0) {
testContinuation.label -= Integer.MIN_VALUE;
break label20;
}
}
testContinuation = new TestContinuation(continuation);
}
Object $result = testContinuation.result;
Object flag = IntrinsicsKt.getCOROUTINE_SUSPENDED();
//變化在這里,這個變量用來存儲fakeSuspendFun的返回值
Object var10000;
switch(testContinuation.label) {
case 0:
ResultKt.throwOnFailure($result);
testContinuation.label = 1;
var10000 = fakeSuspendFun((Continuation)$continuation);
if (var10000 == flag) {
//如果是掛起,那么直接返回COROUTINE_SUSPENDED
return flag;
}
//顯然,這里是不會掛起的,會走這里的break
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
//走這里
Object var1 = var10000;
System.out.println(var1);
return "云天明";
}
}
在調(diào)用偽掛起函數(shù)時,不會掛起,它不會返回COROUTINE_SUSPENDED,而是繼續(xù)往下走。
五、多個掛起函數(shù)前后關(guān)聯(lián)
平時在工作中,可能經(jīng)常會有多個掛起函數(shù)前后是關(guān)聯(lián)的,后面一個掛起函數(shù)需要前面一個掛起函數(shù)的結(jié)果來干點事情,比上面只有一個getUserName掛起函數(shù)稍微復(fù)雜些,我們來分析一下。
比如我們拿到一個需求,展示我的朋友圈,假設(shè)獲取流程如下:獲取用戶id->根據(jù)用戶id獲取該用戶的好友列表->獲取好友列表每個人的朋友圈。下面是非常簡單的實現(xiàn):
//需求: 獲取用戶id->根據(jù)用戶id獲取該用戶的好友列表->獲取好友列表每個人的朋友圈
suspend fun showMoments() {
println("start")
val userId = getUserId()
println(userId)
val friendList = getFriendList(userId)
println(friendList)
val feedList = getFeedList(userId, friendList)
println(feedList)
}
suspend fun getUserId(): String {
delay(1000L)
return "1sa13124daadar2"
}
suspend fun getFriendList(userId: String): String {
println("正在獲取${userId}的朋友列表")
delay(1000L)
return "云天明, 維德"
}
suspend fun getFeedList(userId: String, list: String): String {
println("獲取${userId}的朋友圈($list)")
delay(1000L)
return "云天明: 酒好喝嗎?煙好抽嗎?即使是可口可樂,第一次嘗也不好喝,讓人上癮的東西都是這樣;\n維德: 前進!前進?。〔粨袷侄蔚厍斑M?。。?
}
它的執(zhí)行結(jié)果如下:
start
1sa13124daadar2
正在獲取1sa13124daadar2的朋友列表
云天明, 維德
獲取1sa13124daadar2的朋友圈(云天明, 維德)
云天明: 酒好喝嗎?煙好抽嗎?即使是可口可樂,第一次嘗也不好喝,讓人上癮的東西都是這樣;
維德: 前進!前進??!不擇手段地前進?。。?br />end
這段代碼要稍微復(fù)雜一些,這些掛起函數(shù)前后關(guān)聯(lián),前面獲取到的數(shù)據(jù)后面的掛起函數(shù)需要使用到。相應(yīng)的,它們反編譯之后也要復(fù)雜一些。但是沒關(guān)系,我已經(jīng)把晦澀難懂的代碼重新組裝了一下,方便大家閱讀。同時,在下面的代碼中,每一步在走哪個分支,都有詳細的注釋分析,幫助大家理解。
public final class TestSuspendKt {
@Nullable
public static final Object showMoments(@NotNull Continuation<? super Unit> continuation) {
ShowMomentsContinuation showMomentsContinuation;
label37: {
if (continuation instanceof ShowMomentsContinuation) {
//非第一次進showMoments,則走這里,continuation已經(jīng)是ShowMomentsContinuation了
showMomentsContinuation = (ShowMomentsContinuation)continuation;
if ((showMomentsContinuation.label & Integer.MIN_VALUE) != 0) {
showMomentsContinuation.label -= Integer.MIN_VALUE;
break label37;
}
}
//第一次,走這里,初始化ShowMomentsContinuation,將傳入的continuation包起來
showMomentsContinuation = new ShowMomentsContinuation(continuation);
final class ShowMomentsContinuation extends ContinuationImpl {
int label;
Object result;
//存放臨時數(shù)據(jù)
Object tempData;
ShowMomentsContinuation(Continuation continuation) {
super(continuation);
}
public final Object invokeSuspend(Object obj) {
this.result = obj;
this.label |= Integer.MIN_VALUE;
return CpsTestKt.getUserName(this);
}
}
}
//存放每個函數(shù)的返回結(jié)果,臨時放一下
Object computeResult;
label31: {
String userId;
Object flag;
label30: {
//從continuation中把result取出來
Object $result = showMomentsContinuation.result;
flag = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(showMomentsContinuation.label) {
case 0:
//第一次,走這里,檢測異常
ResultKt.throwOnFailure($result);
System.out.println("start");
//將label改成1
showMomentsContinuation.label = 1;
//執(zhí)行g(shù)etUserId函數(shù),computeResult用來接收返回值
computeResult = getUserId((Continuation)showMomentsContinuation);
//getUserId是掛起函數(shù),不出意外的話,computeResult的值會是COROUTINE_SUSPENDED,這里就直接return了
//showMoments函數(shù)這一次執(zhí)行,就算完成了。
//恢復(fù)執(zhí)行時,會走ShowMomentsContinuation 的invokeSuspend,走下面label等于1的邏輯
if (computeResult == flag) {
return flag;
}
break;
case 1:
//第二次執(zhí)行showMoments時,label已經(jīng)等于1了,走這里.
ResultKt.throwOnFailure($result);
computeResult = $result;
break;
case 2:
//第三次執(zhí)行showMoments時,label已經(jīng)等于2了,走這里.
//先將之前暫存的userId取出來,馬上需要用到
userId = (String)showMomentsContinuation.tempData;
ResultKt.throwOnFailure($result);
computeResult = $result;
break label30;
case 3:
//第四次執(zhí)行showMoments時,label已經(jīng)等于3了,走這里.
ResultKt.throwOnFailure($result);
computeResult = $result;
break label31;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
//第二次執(zhí)行showMoments時,label=1,會走到這里來,將getUserId函數(shù)回調(diào)回來的userId保存起來,并輸出
userId = (String)computeResult;
System.out.println(userId);
//將userId放continuation里面暫存起來
showMomentsContinuation.tempData = userId;
//又要執(zhí)行掛起函數(shù)了,這里將label改成2
showMomentsContinuation.label = 2;
//開始調(diào)用getFriendList
computeResult = getFriendList(userId, (Continuation)showMomentsContinuation);
//getFriendList是掛起函數(shù),不出意外的話,computeResult的值會是COROUTINE_SUSPENDED,這里就直接return了
//showMoments函數(shù)這一次執(zhí)行,就算完成了。
//恢復(fù)執(zhí)行時,會走ShowMomentsContinuation 的invokeSuspend,走上面label等于2的邏輯
if (computeResult == flag) {
return flag;
}
}
//第三次執(zhí)行showMoments時,label=2,會走到這里來,將getFriendList函數(shù)回調(diào)回來的friendList輸出
String friendList = (String)computeResult;
System.out.println(friendList);
showMomentsContinuation.tempData = null;
//又要執(zhí)行掛起函數(shù)了,這里將label改成3
showMomentsContinuation.label = 3;
//開始調(diào)用getFeedList
computeResult = getFeedList(userId, friendList, (Continuation)showMomentsContinuation);
//getFeedList是掛起函數(shù),不出意外的話,computeResult的值會是COROUTINE_SUSPENDED,這里就直接return了
//showMoments函數(shù)這一次執(zhí)行,就算完成了。
//恢復(fù)執(zhí)行時,會走ShowMomentsContinuation 的invokeSuspend,走上面label等于3的邏輯
if (computeResult == flag) {
return flag;
}
}
//第四次執(zhí)行showMoments時,label=3,會走到這里來,將getFeedList函數(shù)回調(diào)回來的feedList輸出
String feedList = (String)computeResult;
System.out.println(feedList);
System.out.println("end");
//showMoments函數(shù)這一次執(zhí)行,就算完成了。
//沒有剩下的掛起函數(shù)需要執(zhí)行了
return Unit.INSTANCE;
}
//因為getUserId、getFriendList、getFeedList中的匿名內(nèi)部類邏輯與showMoments中的一模一樣,故沒有將其重新組織語言
@Nullable
public static final Object getUserId(@NotNull Continuation var0) {
Object $continuation;
label20: {
//這里的<undefinedtype>就是在getUserId函數(shù)里生成的new ContinuationImpl匿名內(nèi)部類
if (var0 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var0;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl(var0) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestSuspendKt.getUserId(this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
//第一次執(zhí)行g(shù)etUserId時,走這里
ResultKt.throwOnFailure($result);
//馬上要開始執(zhí)行掛起函數(shù)了,label先改一下
((<undefinedtype>)$continuation).label = 1;
//執(zhí)行delay,正常情況下,會返回COROUTINE_SUSPENDED,于是getUserId這一次就執(zhí)行完了,return了
//恢復(fù)時會回調(diào)上面的匿名內(nèi)部類$continuation中的invokeSuspend
if (DelayKt.delay(1000L, (Continuation)$continuation) == var3) {
return var3;
}
break;
case 1:
//第二次執(zhí)行g(shù)etUserId時,也就是delay執(zhí)行完回來,走這里
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
//拿到數(shù)據(jù),getUserId就算真正的執(zhí)行完了,接著會去執(zhí)行showMoments函數(shù)中的ShowMomentsContinuation#invokeSuspend,也就是恢復(fù)showMoments,繼續(xù)執(zhí)行showMoments中g(shù)etUserId后面的邏輯
return "1sa13124daadar2";
}
@Nullable
public static final Object getFriendList(@NotNull String userId, @NotNull Continuation var1) {
Object $continuation;
label20: {
if (var1 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var1;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl(var1) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestSuspendKt.getFriendList((String)null, this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
String var2 = "正在獲取" + userId + "的朋友列表";
System.out.println(var2);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(1000L, (Continuation)$continuation) == var5) {
return var5;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "云天明, 維德";
}
@Nullable
public static final Object getFeedList(@NotNull String userId, @NotNull String list, @NotNull Continuation var2) {
Object $continuation;
label20: {
if (var2 instanceof <undefinedtype>) {
$continuation = (<undefinedtype>)var2;
if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
break label20;
}
}
$continuation = new ContinuationImpl(var2) {
// $FF: synthetic field
Object result;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
this.result = $result;
this.label |= Integer.MIN_VALUE;
return TestSuspendKt.getFeedList((String)null, (String)null, this);
}
};
}
Object $result = ((<undefinedtype>)$continuation).result;
Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(((<undefinedtype>)$continuation).label) {
case 0:
ResultKt.throwOnFailure($result);
String var3 = "獲取" + userId + "的朋友圈(" + list + ')';
System.out.println(var3);
((<undefinedtype>)$continuation).label = 1;
if (DelayKt.delay(1000L, (Continuation)$continuation) == var6) {
return var6;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "云天明: 酒好喝嗎?煙好抽嗎?即使是可口可樂,第一次嘗也不好喝,讓人上癮的東西都是這樣;\n維德: 前進!前進?。〔粨袷侄蔚厍斑M?。?!";
}
}
觀察源碼,發(fā)現(xiàn)一些東西:
- 每個掛起函數(shù)都有一個匿名內(nèi)部類,繼承ContinuationImpl,在invokeSuspend中開啟狀態(tài)機
- 每個掛起函數(shù)都經(jīng)過了CPS轉(zhuǎn)換
- 在掛起之后,當(dāng)前執(zhí)行協(xié)程的這個線程其實是空閑的,沒有代碼交給它執(zhí)行。在invokeSuspend恢復(fù)之后,才繼續(xù)執(zhí)行
- 每個掛起函數(shù),都有一個狀態(tài)機
- 掛起函數(shù)中的邏輯被分塊執(zhí)行(也就是狀態(tài)機那塊的邏輯),分塊的數(shù)量=掛起函數(shù)數(shù)量+1
基本上來說,掛起函數(shù)的實現(xiàn)原理就是上面這些了。
六、在Java中調(diào)用suspend函數(shù)
既然Kotlin是兼容Java的,那么如果我想在Java里面調(diào)用Kotlin的suspend函數(shù)按道理也是可以的。那具體如何調(diào)用呢?
就拿上面的案例舉例,假設(shè)我想在Activity中點擊某個按鈕時調(diào)用showMoments這個suspend函數(shù),該怎么搞?大家先思考一下,稍后給出答案。
//將上面的案例加了個返回值
suspend fun showMoments(): String {
println("start")
val userId = getUserId()
println(userId)
val friendList = getFriendList(userId)
println(friendList)
val feedList = getFeedList(userId, friendList)
println(feedList)
println("end")
return feedList
}
因為showMoments函數(shù)有suspend關(guān)鍵字,那么最終會經(jīng)過CPS轉(zhuǎn)換,有一個Continuation參數(shù)。在Java中調(diào)用showMoments時,肯定需要把Continuation傳進去才行。Continuation是一個接口,需要傳個實現(xiàn)類過去,把getContext和resumeWith實現(xiàn)起。
TestSuspendKt.showMoments(new Continuation<String>() {
@NonNull
@Override
public CoroutineContext getContext() {
return (CoroutineContext) Dispatchers.getIO();
}
@Override
public void resumeWith(@NonNull Object result) {
//這里的result就是showMoments的返回值
Log.d("xfhy666", "" + result);
}
});
Java中調(diào)用掛起函數(shù),看起來就像是調(diào)用了一個方法,這個方法需要傳一個callback過去,這個方法的返回值是通過回調(diào)給出來的,并且可以自定義該方法運行在哪個線程中。
七、總結(jié)
好了,今天的Kotlin掛起函數(shù)就分析到這里,基本上謎團已全部解開(除了invokeSuspend是在什么時候回調(diào)的,后面有機會再和大家分享)。
Kotlin的掛起函數(shù),本質(zhì)上就是:CPS+狀態(tài)機。
- CPS:掛起函數(shù)比普通函數(shù)多了suspend關(guān)鍵字,Kotlin編譯器會對其特殊處理。將該函數(shù)轉(zhuǎn)換成一個帶有Callback的函數(shù),Callback就是Continuation接口,它的泛型就是原來函數(shù)的返回值類型。轉(zhuǎn)換之后的返回值類型是
Any?,因為加了suspend關(guān)鍵字的不一定會被掛起,掛起的話返回Intrinsics.COROUTINE_SUSPENDED,偽掛起函數(shù)(里面沒有其他掛起函數(shù),但還是會進行CPS轉(zhuǎn)換)則是直接返回結(jié)果,這個結(jié)果可以是任何類型,所以返回值只能是Any?。 - 狀態(tài)機:當(dāng)掛起函數(shù)經(jīng)過編譯之后,會變成switch和label組成的狀態(tài)機結(jié)構(gòu)。label代表了當(dāng)前狀態(tài)機的具體狀態(tài),每改變一次,就代表掛起函數(shù)被調(diào)用一次。在里面會創(chuàng)建一個Callback接口,當(dāng)掛起之后,掛起函數(shù)的結(jié)果返回是通過Callback回調(diào)回來的,回調(diào)回來之后,因為之前修改過label,根據(jù)該label來判斷該繼續(xù)往下走了,執(zhí)行后面的邏輯。上面的Callback就是Continuation,我覺得它在這里的意思可以翻譯成繼續(xù)執(zhí)行剩余的代碼。
以上就是Kotlin掛起函數(shù)原理示例剖析的詳細內(nèi)容,更多關(guān)于Kotlin掛起函數(shù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android自定義viewgroup可滾動布局 GestureDetector手勢監(jiān)聽(5)
這篇文章主要為大家詳細介紹了Android自定義viewgroup可滾動布局,GestureDetector手勢監(jiān)聽,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12
Android Studio實現(xiàn)簡單的QQ登錄界面的示例代碼
這篇文章主要介紹了Android Studio實現(xiàn)簡單的QQ登錄界面的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
Android Surfaceview的繪制與應(yīng)用
這篇文章主要介紹了Android Surfaceview的繪制與應(yīng)用的相關(guān)資料,需要的朋友可以參考下2017-07-07
Android逆向入門之常見Davlik字節(jié)碼解析
Dalvik是Google公司自己設(shè)計用于Android平臺的虛擬機。Dalvik虛擬機是Google等廠商合作開發(fā)的Android移動設(shè)備平臺的核心組成部分之一,本篇文章我們來詳細解釋常見Davlik字節(jié)碼2021-11-11
Android實現(xiàn)帶有刪除按鈕的EditText示例代碼
本文給大家介紹一個很實用的小控件,就是在Android系統(tǒng)的輸入框右邊加入一個小圖標,點擊小圖標可以清除輸入框里面的內(nèi)容,IOS上面直接設(shè)置某個屬性就可以實現(xiàn)這一功能,但是Android原生EditText不具備此功能,所以要想實現(xiàn)這一功能我們需要重寫EditText。下面來看看吧。2016-12-12

