少年,你可知 Kotlin 協程最初的樣子?

前言

協程系列文章:

如果有人問你,怎么開啟一個 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修飾的函數時:


image.png

意思是:

"suspend"修飾的函數只能在協程里被調用或者是在另一個被"suspend"修飾的函數里調用。

suspend 作用

何為掛起

suspend 意為掛起、阻塞的意思,與協程相關。
當suspend 修飾函數時,表明這個函數可能會被掛起,至于是否被掛起取決于該函數里是否有掛起動作。
比如:

suspend fun testSuspend() {
    println("test suspend")
}

這樣的寫法沒意義,因為函數沒有實現掛起功能。
你可能會說,掛起需要切換線程,好嘛,換個寫法:

suspend fun testSuspend() {
    println("test suspend")
    thread {
        println("test suspend in thread")
    }
}

然而并沒啥用,編譯器依然提示:


image.png

意思是可以不用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")
    }
}

打印結果:


image.png

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)。
我們再回過頭來捋一下類之間關系:


image.png

閉包生成的匿名內部類:

  • 實現了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 就非常容易了,畢竟它們都是提供給開發者更方便操作協程的工具,是在原始攜程的基礎上演變的。
協程創建調用棧簡易圖:


image.png

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 輕松入門系列

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容