前言
協程系列文章:
如果有人問你,怎么開啟一個 Kotlin 協程?你可能會說通過runBlocking/launch/async,回答沒錯,這幾個函數都能開啟協程。不過這次咱們換個角度分析,通過提取這幾個函數的共性,看看他們內部是怎么開啟一個協程的。
相信通過本篇,你將對協程原理有個深刻的認識。
文章目錄:
1、suspend 關鍵字背后的原理
2、如何開啟一個原始的協程?
3、協程調用以及整體流程
4、協程代碼我為啥看不懂?
1、suspend 關鍵字背后的原理
suspend 修飾函數
普通的函數
fun launchEmpty(block: () -> Unit) {
}
定義一個函數,形參為函數類型。
查看反編譯結果:
public final class CoroutineRawKt {
public static final void launchEmpty(@NotNull Function0 block) {
}
}
可以看出,在JVM 平臺函數類型參數最終是用匿名內部類表示的,而FunctionX(X=0~22) 是Kotlin 將函數類型映射為Java 的接口。
來看看Function0 的定義:
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
有一個唯一的方法:invoke(),它沒有任何參數。
可作如下調用:
fun launchEmpty(block: () -> Unit) {
block()//與block.invoke()等價
}
fun main(array: Array<String>) {
launchEmpty {
println("I am empty")
}
}
帶suspend 的函數
以上寫法大家都比較熟悉了,就是典型的高階函數的定義和調用。
現在來改造一下函數類型的修飾符:
fun launchEmpty1(block: suspend () -> Unit) {
}
相較之前,加了"suspend"關鍵字。
老規矩,查看反編譯結果:
public static final void launchEmpty1(@NotNull Function1 block) {
}
參數從Function0 變為了Function1:
/** A function that takes 1 argument. */
public interface Function1<in P1, out R> : Function<R> {
/** Invokes the function with the specified argument. */
public operator fun invoke(p1: P1): R
}
Function1 的invoke()函數多了一個入參。
也就是說,加了suspend 修飾后,函數會默認加個形參。
當我們調用suspend修飾的函數時:
意思是:
"suspend"修飾的函數只能在協程里被調用或者是在另一個被"suspend"修飾的函數里調用。
suspend 作用
何為掛起
suspend 意為掛起、阻塞的意思,與協程相關。
當suspend 修飾函數時,表明這個函數可能會被掛起,至于是否被掛起取決于該函數里是否有掛起動作。
比如:
suspend fun testSuspend() {
println("test suspend")
}
這樣的寫法沒意義,因為函數沒有實現掛起功能。
你可能會說,掛起需要切換線程,好嘛,換個寫法:
suspend fun testSuspend() {
println("test suspend")
thread {
println("test suspend in thread")
}
}
然而并沒啥用,編譯器依然提示:
意思是可以不用suspend 修飾,沒啥意義。
掛起于協程的意義
第一點
當函數被suspend 修飾時,表明協程執行到此可能會被掛起,若是被掛起那么意味著協程將無法再繼續往下執行,直到條件滿足恢復了協程的運行。
fun main(array: Array<String>) {
GlobalScope.launch {
println("before suspend")//①
testSuspend()//掛起函數②
println("after suspend")//③
}
}
執行到②時,協程被掛起,將不會執行③,直到協程被恢復后才會執行③。
注:關于協程掛起的生動理解&線程的掛起 下篇將著重分析。
第二點
如果將suspend 修飾的函數類型看做一個整體的話:
suspend () -> T
無參,返回值為泛型。
Kotlin 里定義了一些擴展函數,可用來開啟協程。
第三點
suspend 修飾的函數類型,當調用者實現其函數體時,傳入的實參將會繼承自SuspendLambda(這塊下個小結詳細分析)。
2、如何開啟一個原始的協程?
launch/async/runBlocking 如何開啟協程
縱觀這幾種主流的開啟協程方式,它們最終都會調用到:
#CoroutineStart.kt
public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
when (this) {
DEFAULT -> block.startCoroutineCancellable(receiver, completion)
ATOMIC -> block.startCoroutine(receiver, completion)
UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
LAZY -> Unit // will start lazily
}
無論走哪個分支,都是調用block的函數,而block 就是我們之前說的被suspend 修飾的函數。
以DEFAULT 為例startCoroutineUndispatched接下來會調用到IntrinsicsJvm.kt里的:
#IntrinsicsJvm.kt
public actual fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
receiver: R,
completion: Continuation<T>
)
該函數帶了倆參數,其中的receiver 為接收者,而completion 為協程結束后調用的回調。
為了簡單,我們可以省略掉receiver。
剛好IntrinsicsJvm.kt 里還有另一個函數:
#IntrinsicsJvm.kt
public actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit>
createCoroutineUnintercepted 為 (suspend () -> T) 類型的擴展函數,因此只要我們的變量為 (suspend () -> T)類型就可以調用createCoroutineUnintercepted(xx)函數。
查找該函數的使用之處,發現Continuation.kt 文件里不少擴展函數都調用了它。
如:
#Continuation.kt
//創建協程的函數
public fun <T> (suspend () -> T).createCoroutine(
completion: Continuation<T>
): Continuation<Unit> =
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
其中Continuation 為接口:
#Continuation.kt
interface Continuation<in T> {
//協程上下文
public val context: CoroutineContext
//恢復協程
public fun resumeWith(result: Result<T>)
}
Continuation 接口很重要,協程里大部分的類都實現了該接口,通常直譯過來為:"續體"。
創建完成后,還需要開啟協程函數:
#Continuation.kt
//啟動協程的函數
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
簡單創建/調用協程
協程創建
由上分析可知,Continuation.kt 里有我們開啟協程所需要的一些基本信息,接著來看看如何調用上述函數。
fun <T> launchFish(block: suspend () -> T) {
//創建協程,返回值為SafeContinuation(實現了Continuation 接口)
//入參為Continuation 類型,參數名為completion,顧名思義就是
//協程結束后(正常返回&拋出異常)將會調用它。
var coroutine = block.createCoroutine(object : Continuation<T> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
//協程結束后調用該函數
override fun resumeWith(result: Result<T>) {
println("result:$result")
}
})
//開啟協程
coroutine.resume(Unit)
}
定義了函數launchFish,該函數唯一的參數為函數類型參數,被suspend 修飾,而(suspend () -> T)定義一系列擴展函數,createCoroutine 為其中之一,因此block 可以調用createCoroutine。
createCoroutine 返回類型為SafeContinuation,通過SafeContinuation.resume()開啟協程。
協程調用
fun main(array: Array<String>) {
launchFish {
println("I am coroutine")
}
}
打印結果:
3、協程調用以及整體流程
協程調用背后的玄機
反編譯初窺門徑
看到上面的打印大家可能比較暈,"println("I am coroutine")"是咋就被調用的?沒看到有調用它的地方啊。
launchFish(block) 接收的是函數類型,當調用launchFish 時,在閉包里實現該函數的函數體即可,我們知道函數類型最終會替換為匿名內部類。
因為kotlin 有不少語法糖,無法一下子直擊本質,老規矩,反編譯看看結果:
public static final void main(@NotNull String[] array) {
launchFish((Function1)(new Function1((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
//閉包里的內容
String var2 = "I am coroutine";
boolean var3 = false;
//打印
System.out.println(var2);
return Unit.INSTANCE;
}
}
@NotNull
public final Continuation create(@NotNull Continuation completion) {
//創建一個Continuation,可以認為是續體
Function1 var2 = new <anonymous constructor>(completion);
return var2;
}
public final Object invoke(Object var1) {
//Function1 接口里的方法
return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
}
}));
}
為了更直觀,刪除了一些不必要的信息。
看到這,你發現了什么?通常傳入函數類型的實參最后將會被編譯為對應的匿名內部類,此時應該編譯為Function1,實現其唯一的函數:invoke(xx),而我們發現實際上還多了兩個函數:invokeSuspend(xx)與create(xx)。
我們有理由相信,invokeSuspend(xx)函數一定在某個地方被調用了,原因是:閉包里打印的字符串:"I am coroutine" 只在該函數里實現,而我們測試的結果是這個打印執行了。
還記得我們上面說的suspend 意義的第三點嗎?
suspend 修飾的函數類型,其實參是匿名內部類,繼承自抽象類:SuspendLambda。
也就是說invokeSuspend(xx)與create(xx) 的定義很有可能來自SuspendLambda,我們接著來分析它。
SuspendLambda 關系鏈
#ContinuationImpl.kt
internal abstract class SuspendLambda(
public override val arity: Int,
completion: Continuation<Any?>?
) : ContinuationImpl(completion), FunctionBase<Any?>, SuspendFunction {
constructor(arity: Int) : this(arity, null)
...
}
該類本身并沒有太多內容,此處繼承了ContinuationImpl類,查看該類也沒啥特殊的,繼續往上查找,找到BaseContinuationImpl類,在里面發現了線索:
#ContinuationImpl.kt
internal abstract class BaseContinuationImpl(
val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
protected abstract fun invokeSuspend(result: Result<Any?>): Any?
open fun create(completion: Continuation<*>): Continuation<Unit> {
}
}
終于看到了眼熟的:invokeSuspend(xx)與create(xx)。
我們再回過頭來捋一下類之間關系:
閉包生成的匿名內部類:
- 實現了Function1 接口,并實現了該接口里的invoke函數。
- 繼承了SuspendLambda,并重寫了invokeSuspend函數和create函數。
你可能會說還不夠直觀,那好,繼續改寫一下:
class MyAnonymous extends SuspendLambda implements Function1 {
int label;
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
String var2 = "I am coroutine";
boolean var3 = false;
System.out.println(var2);
return Unit.INSTANCE;
}
}
public final Continuation create(@NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function1 var2 = new <anonymous constructor>(completion);
return var2;
}
public final Object invoke(Object var1) {
return ((<undefinedtype>)this.create((Continuation)var1)).invokeSuspend(Unit.INSTANCE);
}
}
public static final void launchFish(@NotNull MyAnonymous block) {
Continuation coroutine = ContinuationKt.createCoroutine(block, (new Continuation() {
@NotNull
public CoroutineContext getContext() {
return (CoroutineContext) EmptyCoroutineContext.INSTANCE;
}
public void resumeWith(@NotNull Object result) {
String var2 = "result:" + Result.toString-impl(result);
boolean var3 = false;
System.out.println(var2);
}
}));
//開啟
coroutine.resumeWith(Result.constructor-impl(var3));
}
public static final void main(@NotNull String[] array) {
MyAnonymous myAnonymous = new MyAnonymous();
launchFish(myAnonymous);
}
這么看就比較清晰了,此處我們單獨聲明了一個MyAnonymous類,并構造對象傳遞給launchFish函數。
閉包的執行
既然匿名類的構造清晰了,接下來分析閉包是如何被執行的,也就是查找invokeSuspend(xx)函數是怎么被調用的?
將目光轉移到launchFish 函數本身。
createCoroutine()
先看createCoroutine()函數調用,直接上代碼:
#Continuation.kt
fun <T> (suspend () -> T).createCoroutine(
completion: Continuation<T>
): Continuation<Unit> =
//返回SafeContinuation 對象
//SafeContinuation 構造函數需要2個參數,一個是delegate,另一個是協程狀態
//此處默認是掛起
SafeContinuation(createCoroutineUnintercepted(completion).intercepted(), COROUTINE_SUSPENDED)
#IntrinsicsJvm.kt
actual fun <T> (suspend () -> T).createCoroutineUnintercepted(
completion: Continuation<T>
): Continuation<Unit> {
val probeCompletion = probeCoroutineCreated(completion)
return if (this is BaseContinuationImpl)
//此處的this 即為匿名內部類對象 MyAnonymous,它間接繼承了BaseContinuationImpl
//調用MyAnonymous 重寫的create 函數
//create 函數里new 新的MyAnonymous 對象
create(probeCompletion)
else
createCoroutineFromSuspendFunction(probeCompletion) {
(this as Function1<Continuation<T>, Any?>).invoke(it)
}
}
#IntrinsicsJvm.kt
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
//判斷是否是ContinuationImpl 類型的Continuation
//我們的demo里是true,因此會繼續嘗試調用攔截器
(this as? ContinuationImpl)?.intercepted() ?: this
#ContinuationImpl.kt
public fun intercepted(): Continuation<Any?> =
//查看是否已經有攔截器,如果沒有,則從上下文里找,上下文沒有,則用自身,最后賦值。
//在我們的demo里上下文里沒有,用的是自身
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
最后得出的Continuation 賦值給SafeContinuation 的成員變量:delegate。
至此,SafeContinuation 對象已經構造完畢,接著繼續看如何用它開啟協程。
再看 resume()
#SafeContinuationJvm.kt
actual override fun resumeWith(result: Result<T>) {
while (true) { // lock-free loop
val cur = this.result // atomic read
when {
//初始化狀態為UNDECIDED,因此直接return
cur === CoroutineSingletons.UNDECIDED -> if (SafeContinuation.RESULT.compareAndSet(this,
CoroutineSingletons.UNDECIDED, result.value)) return
//如果是掛起,將它變為恢復狀態,并調用恢復函數
//demo 里初始化狀態為COROUTINE_SUSPENDED,因此會走到這
cur === COROUTINE_SUSPENDED -> if (SafeContinuation.RESULT.compareAndSet(this, COROUTINE_SUSPENDED,
CoroutineSingletons.RESUMED)) {
//delegate 為之前創建的Continuation,demo 里因為沒有攔截,因此為MyAnonymous
delegate.resumeWith(result)
return
}
else -> throw IllegalStateException("Already resumed")
}
}
}
#ContinuationImpl.kotlin
#BaseContinuationImpl類的成員函數
override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> =
try {
//invokeSuspend 即為MyAnonymous 里的方法
val outcome = invokeSuspend(param)
//如果返回值是掛起狀態,則函數直接退出
if (outcome === kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) return
kotlin.Result.success(outcome)
} catch (exception: Throwable) {
kotlin.Result.failure(exception)
}
releaseIntercepted() // this state machine instance is terminating
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
//執行到這,最終執行外層的completion,在demo里會輸出"result:$result"
completion.resumeWith(outcome)
return
}
}
}
}
最后再回頭看 invokeSuspend
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
String var2 = "I am coroutine";
boolean var3 = false;
System.out.println(var2);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
你興許已經發現了,此處的返回值永遠是Unit.INSTANCE啊,那么協程永遠不會掛起。
沒有掛起功能的協程就是雞肋...。
沒錯,咱們的demo里實現的是一個無法掛起的協程,回到最初的launchFish()的調用:
launchFish {
println("I am coroutine")
}
}
因為閉包里只有一個打印語句,根本沒有掛起函數,當然就沒有掛起的說法了。
協程調用整體流程
上面花很多篇幅去分析協程的調用,其實就是為了從kotlin 的簡潔里脫離出來,從而真正了解其背后的原理。
Demo里的協程構造比較原始,相較于launch/async 等啟動方式,它沒有上下文、沒有線程調度,但并不妨礙我們通過它去了解協程的運作。當我們了解了其運作的核心,到時候再去看launch/async/runBlocking 就非常容易了,畢竟它們都是提供給開發者更方便操作協程的工具,是在原始攜程的基礎上演變的。
協程創建調用棧簡易圖:
4、協程代碼我為啥看不懂?
之前有一些小伙伴跟我反饋說:"小魚人,我嘗試去看協程源碼,感覺找不到入口,又或是跟著源碼跟到一半就斷了...你是咋閱讀的啊?"
有一說一,協程源碼確實不太好懂,若要比較順暢讀懂源碼,根據個人經驗可能需要以下前置條件:
1、kotlin 語法基礎,這是必須的。
2、高階函數&擴展函數。
3、平臺代碼差異,有一些類、函數是與平臺相關,需要定位到具體平臺,比如SafeContinuation,找到Java 平臺的文件:SafeContinuationJvm.kt。
4、斷點調試時,有些單步斷點不會進入,需要指定運行到的位置。
5、有些代碼是編譯時期構造的,需要對照反編譯結果查看。
6、還有些代碼是沒有源碼的,可能是ASM插入的,此時只能靠肉眼理解了。
如果你對kotlin 基礎/高階函數 等有疑惑,請查看之前的文章。
本篇僅僅構造了一個簡陋的協程,協程的最重要的掛起/恢復并沒有涉及,下篇將會著重分析如何構造一個掛起函數,以及協程到底是怎么掛起的。
本文基于Kotlin 1.5.3,文中完整Demo請點擊
您若喜歡,請點贊、關注,您的鼓勵是我前進的動力
持續更新中,和我一起步步為營系統、深入學習Android/Kotlin
1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發全套服務
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執行原因
8、Android事件驅動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎系列
18、Android Jetpack 易懂易學系列
19、Kotlin 輕松入門系列