Kotlin 協(xié)程啟動篇:靜態(tài)代理分層

前段時(shí)間在項(xiàng)目中引入了 Kotlin Coroutine令漂,那么也來談?wù)剬λ睦斫饴タ取K^窺一斑而知全豹娃弓,首先嘗試透過一個(gè)??來窺探協(xié)程的啟動流程辟汰,直接通過 GlobalScope.launch 啟動了一個(gè)頂級協(xié)程并指定在主線程中執(zhí)行:

fun test() {
    GlobalScope.launch(Dispatchers.Main) {
        Log.d("GlobalScopeTest", "Log in GlobalScope")
    }
    Log.d("GlobalScopeTest", "Log after GlobalScope")
}

上面的代碼輸出結(jié)果為:

Log after GlobalScope
Log in GlobalScope

為什么會出現(xiàn)這樣的執(zhí)行順序列敲?協(xié)程體內(nèi)的代碼邏輯是透過什么機(jī)制分發(fā)到主線程執(zhí)行?要解答這些問題帖汞,還得從 GlobalScope.launch 尋找突破口戴而。

1、GlobalScope.launch:協(xié)程入口

launch 是個(gè)擴(kuò)展函數(shù)翩蘸、啟動協(xié)程并且返回一個(gè) job 對象所意。這里先大概了解返回的 job 可以方便地對協(xié)程進(jìn)行追蹤,取消等操作催首。

launch 源碼:

public fun CoroutineScope.launch(
   context: CoroutineContext = EmptyCoroutineContext,
   start: CoroutineStart = CoroutineStart.DEFAULT,
   block: suspend CoroutineScope.() -> Unit
): Job {
   val newContext = newCoroutineContext(context)
   // 構(gòu)建 AbstractCoroutine 對象扶踊,并由此開啟協(xié)程任務(wù)
   val coroutine = if (start.isLazy)
       LazyStandaloneCoroutine(newContext, block) else
       StandaloneCoroutine(newContext, active = true)
   coroutine.start(start, coroutine, block)
   return coroutine
}

看了 launch 方法,會有一個(gè)疑惑:經(jīng)過編譯后郎任,協(xié)程把我們的代碼邏輯封裝成了一個(gè) SuspendLambda 類秧耗,那么必然需要該類的相關(guān)信息才可能進(jìn)行回調(diào)執(zhí)行邏輯,但是在入口處并沒有看到相關(guān)參數(shù)涝滴,難道還有什么黑科技能夠獲取到類相關(guān)信息绣版?

事實(shí)上反編譯 class 文件,發(fā)現(xiàn) CoroutineScope.launch 方法簽名和源碼實(shí)現(xiàn)略有不同歼疮,其中多了一個(gè)參數(shù):Function2<? super CoroutineScope, ? super Continuation<? super T>, ? extend Object>杂抽。 而生成的 SuspendLambda 類就是 Function2 的實(shí)現(xiàn)類。

沒錯(cuò)韩脏,在調(diào)用 CoroutineScope.launch 的時(shí)候缩麸,傳入的 Function2 的參數(shù)值就是生成的 SuspendLambda 對象!所以在這里并沒有什么黑科技赡矢,只是由于編譯器的參與杭朱,隱藏了其中一些細(xì)節(jié)。

反編譯得到的 launch 源碼:

@NotNull
public static final Job launch(@NotNull CoroutineScope $this$launch, @NotNull CoroutineContext context, @NotNull CoroutineStart start, @NotNull Function2<? super CoroutineScope, ? super Continuation<? super Unit>, ? extends Object> block) {
   StandaloneCoroutine coroutine;
   Intrinsics.checkParameterIsNotNull($this$launch, "$this$launch");
   Intrinsics.checkParameterIsNotNull(context, "context");
   Intrinsics.checkParameterIsNotNull(start, "start");
   Intrinsics.checkParameterIsNotNull(block, "block");
   CoroutineContext newContext = CoroutineContextKt.newCoroutineContext($this$launch, context);
   if (start.isLazy()) {
        coroutine = new LazyStandaloneCoroutine(newContext, block);
   } else {
       coroutine = new StandaloneCoroutine(newContext, true);
   }
   coroutine.start(start, coroutine, block);
   return coroutine;
}

回到 launch 吹散,方法最后調(diào)用 start 啟動了協(xié)程弧械。層層追蹤最后調(diào)用了 startCoroutineCancellable:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
    }

觀察方法名大概能得出一個(gè)判斷:

  1. createCoroutineUnintercepted:首先創(chuàng)建了一個(gè)非攔截的協(xié)程 Continuation。
  2. intercepted:添加攔截器 Continuation空民。
  3. resume:正式啟動協(xié)程刃唐。

看來要知道更多細(xì)節(jié)羞迷,還要從這幾個(gè)方法入手。

2画饥、createCoroutineUnintercepted:構(gòu)建任務(wù)層對象

createCoroutineUnintercepted 在 jvm 的實(shí)現(xiàn)在 IntrinsicsJvm 類中衔瓮。在這里,launch 處傳入的 Function2 參數(shù)起了作用抖甘,通過這個(gè)對象調(diào)用 create() 來構(gòu)建 編譯器生成的 SuspendLambda 對象热鞍。

@SinceKotlin("1.3")
public fun <R, T> (suspend R.() -> T).createCoroutineUnintercepted(
    receiver: R,
    completion: Continuation<T>
): Continuation<Unit> {
    val probeCompletion = probeCoroutineCreated(completion)
    return if (this is BaseContinuationImpl)
         // 創(chuàng)建 SuspendLambda 對象
        create(receiver, probeCompletion)
    else {
        createCoroutineFromSuspendFunction(probeCompletion) {
            (this as Function2<R, Continuation<T>, Any?>).invoke(receiver, it)
        }
    }
}

反編譯得到的 create 方法 :

@NotNull
public final Continuation<Unit> create(@Nullable Object value, @NotNull Continuation<?> completion) {
    Intrinsics.checkParameterIsNotNull(completion, "completion");
    
    // 將Continuation傳入作為completion參數(shù)值(靜態(tài)代理)
    Continuation coroutineTest$test$1 = new CoroutineTest$test$1(this.$context, completion);
    CoroutineScope coroutineScope = (CoroutineScope) value;
    coroutineTest$test$1.p$ = (CoroutineScope) value;
    return coroutineTest$test$1;
}

所以 launch 傳入的 Function2 參數(shù)作用是構(gòu)建出編譯器為我們生成的 SuspendLambda 對象,后續(xù)才能回調(diào)掛起的邏輯衔彻。那么 SuspendLambda.intercepted() 又做了什么薇宠?

3、Continuation.intercepted:構(gòu)建任務(wù)調(diào)度層對象

追蹤 intercepted 調(diào)用鏈艰额,最終調(diào)用了 ContinuationImpl.intercepted :

public fun intercepted(): Continuation<Any?> =
    intercepted ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }

上面代碼關(guān)鍵點(diǎn)就在于 (context[ContinuationInterceptor]?.interceptContinuation(this)昼接,context[ContinuationInterceptor] 得到的是 HandlerContext 對象(why?)而 handlerContext 是 CoroutineDispatcher 的子類(CoroutineDispatcher 可以理解為線程級別的任務(wù)調(diào)度類悴晰,決定了任務(wù)在哪個(gè)線程中執(zhí)行)慢睡,CoroutineDispatcher.interceptContinuation() 返回了 DispatchedContinuation 對象。

why HandlerContext handlerContext = context[ContinuationInterceptor] ?

栗子中我們調(diào)用 launch 傳入了 Dispatchers.Main 作為 context铡溪,CoroutineContext 可以理解為一個(gè)數(shù)據(jù)結(jié)構(gòu)漂辐,其中鍵為 ContinuationInterceptor,值為 Dispatchers.Main棕硫。所以context[ContinuationInterceptor] 得到的是 Dispatchers.Main髓涯,即為 HandlerContext。

intercepted 就是通過靜態(tài)代理得到了 DispatchedContinuation 對象哈扮。接下來只需要通過 resumeCancellable 開啟協(xié)程纬纪。

4、DispatchedContinuation.resumeCancellable:開啟協(xié)程任務(wù)

DispatchedContinuation 為任務(wù)調(diào)度類滑肉,將具體任務(wù)分配給 Dispatcher(也就是上面通過 context[ContinuationInterceptor] 得到的對象) 執(zhí)行包各。resumeCancellable 內(nèi)部調(diào)用 dispatcher.dispatch 將任務(wù)交給了 Dispatcher,方法第二個(gè)參數(shù) this 指代 runnable 對象靶庙。

resumeCancellable 的實(shí)現(xiàn):

@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
inline fun resumeCancellable(value: T) {
   // 指定需要線程調(diào)度问畅,默認(rèn)為true
   if (dispatcher.isDispatchNeeded(context)) {
       _state = value
       resumeMode = MODE_CANCELLABLE
       dispatcher.dispatch(context, this)
   } else {
   // 不需要線程調(diào)度
       executeUnconfined(value, MODE_CANCELLABLE) {
           if (!resumeCancelled()) {
                resumeUndispatched(value)
           }
       }
   }
}

HandlerContext 的 dispatch 方法實(shí)現(xiàn)如下,通過 handler 把任務(wù)拋出去:

override fun dispatch(context: CoroutineContext, block: Runnable) {
    handler.post(block)
}

DispatchedContinuation 內(nèi) runnable.run 方法實(shí)現(xiàn)如下:

public final override fun run() {
   val taskContext = this.taskContext
   var exception: Throwable? = null
   try {
       val delegate = delegate as DispatchedContinuation<T>
       val continuation = delegate.continuation
       val context = continuation.context
       val job = if (resumeMode.isCancellableMode) context[Job] else null
       val state = takeState() // NOTE: Must take state in any case, even if cancelled
       withCoroutineContext(context, delegate.countOrElement) {
           if (job != null && !job.isActive) {
               val cause = job.getCancellationException()
               cancelResult(state, cause)
               continuation.resumeWithStackTrace(cause)
           } else {
               val exception = getExceptionalResult(state)
               if (exception != null)
                   continuation.resumeWithStackTrace(exception)
               else
                 // 回調(diào)suspendLambda.resumeWith()
                   continuation.resume(getSuccessfulResult(state))
           }
       }
   } catch (e: Throwable) {
        // This instead of runCatching to have nicer stacktrace and debug experience
      exception = e
   } finally {
      val result = runCatching { taskContext.afterTask() }
      handleFatalException(exception, result.exceptionOrNull())
   }
}

run 方法內(nèi)部通過調(diào)用 continuation.resume 回調(diào)整條鏈路的任務(wù)邏輯六荒。

continuation.resume 的主要下游調(diào)用鏈路:continuation.resume() -> continuation.resumeWith() -> suspendLambda.invokeuspend() -> AbstractContinue.resumeWith()护姆。

其中 suspendLambda.invokeuspend 內(nèi)部封裝的就是我們栗子中的邏輯: Log.d("GlobalScopeTest", "Log in GlobalScope")

從 launch 開啟協(xié)程到如何回調(diào)到我們的邏輯大致流程已經(jīng)理清。由于這個(gè)栗子指定了主線程作為運(yùn)行環(huán)境掏击。還有更多關(guān)于協(xié)程如何進(jìn)行線程調(diào)度卵皂、多線程下函數(shù)掛起、恢復(fù)等流程在這里未涉及到砚亭。不過我們大概可以推測對于指定IO線程執(zhí)行的協(xié)程體灯变,協(xié)程必然維護(hù)了自己的線程池作為協(xié)程的運(yùn)行環(huán)境豺旬。

小結(jié)

  1. launch 方法還有一個(gè)隱式參數(shù):Function2,也就是編譯器生成 SuspendLambda 的子類對象柒凉。為后續(xù)構(gòu)建真實(shí)可用的 SuspendLambda 對象做鋪墊。
  2. 通過靜態(tài)代理構(gòu)建了三層 Continuation 分別負(fù)責(zé)不同的職責(zé):AbstractCoroutine(協(xié)程狀態(tài)處理層) -> SuspendLambda(邏輯執(zhí)行層) -> CoroutineDispatcher(任務(wù)調(diào)度層)
  3. 任務(wù)最終交由 CoroutineDispatcher 執(zhí)行篓跛。最終脫離不開線程級別的任務(wù)調(diào)度膝捞。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市愧沟,隨后出現(xiàn)的幾起案子蔬咬,更是在濱河造成了極大的恐慌,老刑警劉巖沐寺,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件林艘,死亡現(xiàn)場離奇詭異,居然都是意外死亡混坞,警方通過查閱死者的電腦和手機(jī)狐援,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來究孕,“玉大人啥酱,你說我怎么就攤上這事〕睿” “怎么了镶殷?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長微酬。 經(jīng)常有香客問我绘趋,道長,這世上最難降的妖魔是什么颗管? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任陷遮,我火速辦了婚禮,結(jié)果婚禮上垦江,老公的妹妹穿的比我還像新娘拷呆。我一直安慰自己,他們只是感情好疫粥,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布茬斧。 她就那樣靜靜地躺著,像睡著了一般梗逮。 火紅的嫁衣襯著肌膚如雪项秉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天慷彤,我揣著相機(jī)與錄音娄蔼,去河邊找鬼怖喻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岁诉,可吹牛的內(nèi)容都是我干的锚沸。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼涕癣,長吁一口氣:“原來是場噩夢啊……” “哼哗蜈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坠韩,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤距潘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后只搁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體音比,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年氢惋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了洞翩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡焰望,死狀恐怖菱农,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柿估,我是刑警寧澤循未,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站秫舌,受9級特大地震影響的妖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜足陨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一嫂粟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧墨缘,春花似錦星虹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蝶棋,卻和暖如春卸亮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背玩裙。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工兼贸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留段直,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓溶诞,卻偏偏與公主長得像鸯檬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子螺垢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容