Kotlin協程源碼閱讀總結

前段時間面試聊到了協程 , 但自己又很久沒去閱讀協程相關的源碼 ,所以回答的并不是很好。

過程:


面試官 : 項目中怎么處理多線程相關的

xxx: 使用Kolint協程來處理的

面試官:那你說說協程是怎么使用的

xxx : Activity 和 ViewModel 都有協程域,直接調用launch方法使用就行

面試官 :那在Application 或者 service 中如何使用呢

xxx:可以使用GlobalScope

面試官 : 那你說說GlobalScope 有什么注意事項?

xxx : emm。。。

想要回答好這個問題,那肯定是對協程必須要知根知底。 正好這幾天看了些相關文章,所以想寫篇文章總結一下,希望能幫到大家。

協程是什么?

用輕量級線程來回答其實并不準確,因為協程并不是繼承自線程,而是運行在線程之上的。每一個協程都實現了Continuation接口 , 這個接口里面有個resumeWith 方法和context (這個context不是android 中activity繼承那個context)。Continuation有很多子類,最低級的子類是SuspendLambda。協程域里 launch/async/withContext等 里面的代碼經過編譯器編譯之后都存在SuspendLambda中。說了這么多,所以協程也可以理解成若干個Continuation協作構成的程序。

協程方法必須使用suspend 標記,suspend標記的方法經過kt編譯器CPS轉換后,會在方法末尾添加一個參數Continuation,和將方法的返回值修改為Object 。

SuspendLambda

SuspendLambda會實現Function2接口, 因為 suspend CoroutineScope.() -> Unit 經過kt編譯器在java對應著的就是Function2<CoroutineScope, Continuation<? super Unit>, Object> ,所以CoroutineScope.launch(

context: CoroutineContext = EmptyCoroutineContext,

start: CoroutineStart = CoroutineStart.DEFAULT,

block: suspend CoroutineScope.() -> Unit

)方法最后的那個傳入block,在java就是 MainActivityonCreateonCreateonCreate1,如下:

final class MainActivity$onCreate$1 extends SuspendLambda implements Function2<CoroutineScope, Continuation<? super Unit>, Object> {
    int label;

    MainActivity$onCreate$1(Continuation<? super MainActivity$onCreate$1> continuation) {
        super(2, continuation);
    }

    public final Continuation<Unit> create(Object obj, Continuation<?> continuation) {
        return new MainActivity$onCreate$1<>(continuation);
    }

    public final Object invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {
        return ((MainActivity$onCreate$1) create(coroutineScope, continuation)).invokeSuspend(Unit.INSTANCE);
    }

    public final Object invokeSuspend(Object $result) {
        MainActivity$onCreate$1 mainActivity$onCreate$1;
        Object coroutine_suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
        switch (this.label) {
            case 0:
                ResultKt.throwOnFailure($result);
                mainActivity$onCreate$1 = this;
                long r2 = LiveLiterals$MainActivityKt.INSTANCE.m135xec86fb79();
                Continuation continuation = mainActivity$onCreate$1;
                mainActivity$onCreate$1.label = 1;
                if (DelayKt.delay(r2, continuation) == coroutine_suspended) {
                    return coroutine_suspended;
                }
                break;
            case 1:
                mainActivity$onCreate$1 = this;
                ResultKt.throwOnFailure($result);
                break;
            case 2:
                ResultKt.throwOnFailure($result);
                break;
            default:
                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
        }
        System.out.println(LiveLiterals$MainActivityKt.INSTANCE.m139x894324b7() + Thread.currentThread().getName());
        long r22 = LiveLiterals$MainActivityKt.INSTANCE.m136x7689ba5d();
        Continuation continuation2 = mainActivity$onCreate$1;
        mainActivity$onCreate$1.label = 2;
        if (DelayKt.delay(r22, continuation2) == coroutine_suspended) {
            return coroutine_suspended;
        }
        MainActivity$onCreate$1 mainActivity$onCreate$12 = mainActivity$onCreate$1;
        return Unit.INSTANCE;
    }
}

如果需要切換線程則使用DispatchedContinuation , 這個也是Continuation的子類,而且這個類是繼承自Runnable的,如果需要切換線程則把SuspendLambda包裝在這個類里面。

如果在當前線程執行則直接調用SuspendLambda#resumeWith方法就行了,resumeWith方法實現在BaseContinuationImpl中。好了暫時先粗略了解這兩個子類,關于協程是什么就先說到這里了。

協程作用域

GlobalScope是不支持cancel的,但是GlobalScope.launch會返回一個job,這個job是支持取消的。因為GlobalScope 的EmptyCoroutineContext里是沒有Job的。所以更推薦使用 CoroutineScope(Dispatchers.Default) ,這個會在context上加上Job 。

MainScope 是在主線程使用的協程作用域,因此在這個域里不能執行耗時操作的,如果要執行耗時操作必須要啟動子協程并且指定調度器。

協程的啟動

協程必須在協程域里面啟動,有四種啟動方式DEFAULT 、 ATOMIC、UNDISPATCHED、LAZY ,DEFAULT 啟動 是支持取消的。協程啟動之前先要經過協程調度器去調度到對應的線程,之后才執行協程體內的代碼。

launch啟動協程不是lazy情況,每次都會新建StandaloneCoroutine,StandaloneCoroutine繼承AbstractCoroutine,AbstractCoroutine繼承自JobSupport和實現Continuation,這個可以理解為頂級協程,這個協程支持cancel或者其他job支持的操作。

withContext 啟動協程,會掛起當前協程直到獲取到返回值,才恢復當前協程執行。

async 啟動協程不會掛起當前協程 ,會返回一個Deferred,調用Deferred#await方法如果返回值還沒準備好會掛起當前協程。

所以總結下: 這么多啟動子協程無非就兩種方式,一種掛起當前協程啟動,另一種是不掛起當前協程啟動。

協程調度器

DEFAULT 調度器 ,通過CoroutineScope.launch啟動的時候會先構建出協程上下文,調度器為 Dispatchers.Default 即默認調度器 ,Dispatchers.Default 是一個單例 ,里面的線程數量和當前手機的cpu核數相等。如果是雙核的話,調度器為默認調度器的情況,協程里面的代碼只能在兩個線程跑(不信可以通過Thread.sleep去測試),所以請求網絡只用這個調度器肯定不行,兩個線程不夠跑。

IO調度器,里面最少有64個線程,網絡請求、IO操作都可以使用這個調度器,并且這個調度器也會用到默認調度器中的線程(資源利用最大化)。

調度器中的Worker數量即線程數量,每個Worker有它自己的本地隊列,這個隊列是一個生產者消費者隊列,最大的緩沖閾值為128。

協程執行流程

回到最開始那個代碼片段,無論通過什么scope.launch啟動協程 ,其實都是調用CoroutineScope的擴展方法launch。通過withContext開始協程會調用到suspendCoroutineUninterceptedOrReturn會掛起當前協程。

以哪種方式啟動協程最終都會執行代碼(以默認啟動為例), block.startCoroutineCancellable(completion) ,這個又會執行createCoroutineUnintercepted(completion).intercepted().resumeCancellableWith(Result.success(Unit))

。 createCoroutineUnintercepted 這個會執行上面那個代碼的create方法獲取到MainActivityonCreateonCreateonCreate1,completion就是頂級協程,在當前協程執行后,即invokeSuspend方法執行完,會調用頂級協程的resumeWith方法。頂級協程的invokeSuspend方法執行完當前協程域的所有協程就結束了。

intercepted()方法如果需要調度線程則會將協程包裝成DispatchedContinuation,所有前提都準備好了會調用當前協程的resumeWith方法。

internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!! // fail fast when trying to resume continuation without completion
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() // this state machine instance is terminating
                if (completion is BaseContinuationImpl) {
                    // unrolling recursion via loop
                    current = completion
                    param = outcome
                } else {
                    // 調用父協程的resumeWith的方法
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }
    //要執行的協程代碼體
    protected abstract fun invokeSuspend(result: Result<Any?>): Any?

resumeWith里面有個死循環,執行完invokeSuspend 方法,返回為COROUTINE_SUSPENDED則需要掛起,掛起則直接調用return 退出當前方法,所以協程掛起也沒多少神秘就是return結束當前方法去執行子協程,并把當前協程傳給子協程,子協程resumeWith方法中,因為是while(true),在執行完自身invokeSuspend方法后把 current = completion ,又恢復到當前協程執行當前協程的invokeSuspend方法。completion 如果不是BaseContinuationImpl則是頂級協程,頂級協程繼承自AbstractCoroutine,所有子協程都是繼承自SuspendLambda, SuspendLambda又是繼承自BaseContinuationImpl。所以調用完頂級協程的completion.resumeWith(outcome),return當前協程域的協程就執行完了。

總結一下:resumeWith這個死循環要跳出只能是掛起當前協程或者是執行完頂級協程的resumeWith()方法。

再來說一下其他的幾個協程中常用的api

delay 方法

這個方法就是用來掛起當前的協程的,并且支持取消掛起。但是delay方法掛起并不會阻塞主線程,因為這個內部通過另開一個線程配合DelayedTaskQueue隊列來實現的,并不會影響主線程。

delay內部也是通過suspendCancellableCoroutine實現。

suspendCancellableCoroutine、suspendCoroutine

這兩個方法會掛起當前協程,去執行耗時操作,當耗時操作執行完恢復當前協程執行的時候就可以獲取到suspendCancellableCoroutine、suspendCoroutine的返回值,所以一般用于和其他庫做適配比如retrofit,注意這兩個方法內部并不會開啟子協程 。

retrofit 中使用如下

@JvmName("awaitNullable")
suspend fun <T : Any> Call<T?>.await(): T? {
    return suspendCancellableCoroutine { continuation ->
        continuation.invokeOnCancellation {
            cancel()
        }
        enqueue(object : Callback<T?> {
            override fun onResponse(call: Call<T?>, response: Response<T?>) {
                if (response.isSuccessful) {
                    continuation.resume(response.body())
                } else {
                    continuation.resumeWithException(HttpException(response))
                }
            }
            
            override fun onFailure(call: Call<T?>, t: Throwable) {
                continuation.resumeWithException(t)
            }
        })
    }
}

enqueue 是異步方法,這里把異步處理完請求后通過continuation.resume 系列方法回到當前協程,執行當前協程的invokeSuspend方法。

GlobalScope正確使用

如果在很多處通過GlobalScope.launch啟動協程,這樣會造成協程非常難管理,因為不能通過頂級域GlobalScope去取消協程,而且這種方式啟動的生命周期跟隨應用的生命周期,非常容易造成內存泄漏。

如果真要使用GlobalScope的話,可以把GlobalScope.launch啟動協程的返回值job都保存在map中,自己管理這些job的狀態,在協程需要取消的時候從map移除job并調用其cancel方法。

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

推薦閱讀更多精彩內容