Coroutine 學(xué)習(xí)(三)Continuation & CoroutineInterceptor & Dispatcher
在最近的項目中有很深刻的體會,Kotlin coroutine給開發(fā)者提供了一個非常好用、簡單、調(diào)試方便的協(xié)程api,但是作為一個想不斷精進自己code質(zhì)量的人,就必須弄懂coroutine的底層原理。所以在前兩篇內(nèi)容 ->?
DEVLOG Coroutine 學(xué)習(xí)(一)掛起,CoroutineScope,異常
Coroutine 學(xué)習(xí)(二)ViewModelScope LifeCycleScope Dispatcher
的基礎(chǔ)上,我想先看看在使用常見的那些api,如launch runblocking..... 繞不過去的基礎(chǔ)知識:
參考內(nèi)容:
(1)Kotlin協(xié)程-協(xié)程的內(nèi)部概念Continuation:?
https://blog.csdn.net/weixin_42063726/article/details/106198212
(2)Kotlin協(xié)程實現(xiàn)原理系列:
https://zhuanlan.zhihu.com/p/301494587

本文主要討論幾個要點
對于協(xié)程中的重要概念: Continuation起一個頭
仔細探討一下Coroutine中的CoroutineInterceptor和Dispatchers
PART1: Continuation
Continuation在Kotlin協(xié)程中有著明確的定義:
其主要作用是表示協(xié)程掛起,并且繼續(xù)執(zhí)行的一種行為。Continuation對象好像和我們隔得很遠,但是在Kotlin coroutine代碼編譯的過程中,編譯器會對suspend 函數(shù)以及suspend lambda進行操作,使它們包含Continuation。這個內(nèi)容將會在下一篇文章中討論。

PART2: CoroutineContext源碼分析
需要注意的是Continuation中包含了一個CoroutineContext對象。 之前的DEVLOG Coroutine 學(xué)習(xí)(一)掛起,CoroutineScope,異常中說過,CoroutineScope中也包含了CoroutineContext,并且指出了CoroutineContext中包含了多種Element。

下面我們結(jié)合具體的代碼分析.
我們可以通過 + 的方式構(gòu)建一個CoroutineContext,并且可以通過類似于Kotlin map中的索引標識符獲取放在coroutineContext中的對象:

下面我們主要分析一下 CoroutineContext中 +,也就是plus方法和索引方法的實現(xiàn)。
CoroutineContext.Element
首先要看看CoroutineContext中的Element:
可以看出Element其實也是一個CoroutineContext,通過重載的get方法,傳入一個類型參數(shù),我們就可以使用[]取出想要的數(shù)據(jù)。
CoroutineContext#plus
這個plus方法,比較容易理解,首先會判斷當前的context是否是EmptyCoroutineContext,
如果不是這樣,就會使用CoroutineContext#fold方法,將傳入的elment添加到Context中:
如果存在ContinuationInterceptor,這個方法會確保ContinuationInterceptor在Context的最后一個。所以CoroutineContext這個interface的底層是通過自身的一個實現(xiàn)類,即CombinedContext。
CombinedContext是一個這樣的結(jié)構(gòu):
它自身只有l(wèi)eft和element兩個元素,left表示之前添加過的所有的CoroutineContext想加起來的結(jié)果,element表示待添加CoroutineContext實例。 整體結(jié)構(gòu)類似于:
((Job), CoroutineName) + Element -> (((Job), CoroutineName),?Element)
同時需要注意,CoroutineContext(CombinedContext)在通過key尋找元素的時候,是從從最右邊向最左邊尋找。之所以存在這個特性,這是因為CoroutineContext對于CoroutineInterceptor的尋找和使用非常頻繁。下面通過一個例子了解一下CoroutineInterceptor。

PART3:?CoroutineInteceptor & Dispatchers
在下面的例子中,我手寫了一個interceptor。之前稍微有提及,在suspend 函數(shù)和suspend lambda編譯過程中,都會插入Continuation,用于干預(yù)協(xié)程的掛起和恢復(fù)過程:
對于這個Interceptor,我們放到具體的例子中進行分析:
因為ContinuationInterceptor可以對于協(xié)程的掛起和恢復(fù)進行監(jiān)聽,我們可以看到,對于父協(xié)程的Continuation(后續(xù)稱為父Continuation),執(zhí)行到通過launch構(gòu)造子協(xié)程時,會同樣攔截到子Continuation的行為,直到子Continuation完成操作,父Continuation才會結(jié)束。

我們再來看看這個例子:
在這個例子中,我只是將子協(xié)程的dispatchers替換成了Kotlin提供的Dispatchers.Main,但是,并沒有發(fā)現(xiàn)攔截的代碼出現(xiàn)。?
如果仔細看一下Dispatchers的相關(guān)的類的繼承關(guān)系,可以發(fā)現(xiàn)Dispatchers也是CoroutineInterceptor,所以Dispatchers相應(yīng)的實現(xiàn)中肯定有對應(yīng)的interceptContinuation實現(xiàn)。這不難理解,Dispatchers的作用就是進行線程切換。

所以接下來,我們需要分析兩個問題
CoroutineInterceptor#interceptContinuation是在哪里被調(diào)用的?
既然Dispatchers也是CoroutineInterceptor,那我們就需要看看它的interceptContinuation的實現(xiàn)。?

CoroutineInterceptor#interceptContinuation的調(diào)用鏈
我們從launch方法開始往下看,這個是相關(guān)的方法的調(diào)用鏈,接下來我們一步一步在源碼中解析:

CoroutineScope.launch:
我們默認的協(xié)程都說不是Lazy的,使用launch方法都會立刻開始執(zhí)行協(xié)程,所以接下來會將StandaloneCoroutine創(chuàng)建的協(xié)程實例返回給coroutine,然后得到的coroutine對象會執(zhí)行start。?
這里需要仔細看看,start中會使用傳入的CoroutineStart類型的start lambda(姑且這么解釋)來繼續(xù)執(zhí)行(也就是會執(zhí)行invoke方法),這個start對象是構(gòu)造launch的時候的參數(shù):
默認情況下是CoroutineStart.DEFAULT,繼續(xù)跟蹤CoroutineStart中的invoke方法:
block 是一個 suspend R.() -> T 類型的高階函數(shù),所以startCoroutineCancellable一定是block的擴展方法,:
startCoroutineCancellable 最終會轉(zhuǎn)到CoroutineImpl中的intercepted。
在這個方法中,當前的CoroutineContext中定義的ContinuationInterceptor會被取出,具體的取出的邏輯在上面已經(jīng)說過。 針對這個Interceptor,就會執(zhí)行它的interceptContinuation實現(xiàn)。

這里小結(jié)一下,在上面的例子中,我們自定義的interceptor,通過CoroutineContext的+重載方法被放倒了CoroutineContext的最后一個。自定義的interceptor然后會被CoroutineContext的get(key)重載取出,并且執(zhí)行它的interceptContinuation方法。
既然是這樣,如果我們在launch的時候指定Dispatchers,也相當于指定了CoroutineInterceptor,那我們看下常見的Dispatchers.Main 和 Dispatchers.DEFAULT中interceptContinuation的實現(xiàn)。

DispatchedContinuation
Dispatchers的共有的父類是CoroutineDispatcher,它實現(xiàn)了interceptContinuation方法:
? ? (上面寫錯了,resumeWith方法應(yīng)該是DispatchedContinuation中的)
顯而易見的是,interceptContinuation中需要返回的Continuation對象被DispatchedContinuation實現(xiàn),所以我們需要看看它的resumeWith方法中干了什么。
resumeWith方法會通過是否需要分發(fā),來分別調(diào)用dispatcher對象的dispatch方法和continuation的resumeWith方法。值得注意的是,這兩個對象都來自于構(gòu)造DispatchedContinuation對象時的參數(shù)。

換言之,如果需要分發(fā),就會調(diào)用參數(shù)中的continuation,這個參數(shù)從哪里來?個人認為應(yīng)該是在構(gòu)建suspend 函數(shù)和suspend lambda的時候通過kotlin編譯器創(chuàng)建的;如果不需要分發(fā),就需要看dispatcher對象的dispatcher邏輯。
因此,現(xiàn)在的分析的著力點就轉(zhuǎn)向了具體的dispatcher對象,也就是我們傳入的Disaptchers.Main Dispatchers.IO 這些中的dispatch方法,我們看看它們到底是怎么進行分發(fā)的。
Dispatchers.Main
Dispatchers.?Main通過loadMainDispatcher創(chuàng)建dispatcher,實際上loadMainDispatcher通過調(diào)用創(chuàng)建Dispatchers的工廠方法構(gòu)建Dipatchers對象:
上述方法中創(chuàng)建的出來的DispatcherFactory其實是AndroidDispatcherFactory。AndroidDispatcherFactory構(gòu)造了HandlerContext。之前說過HandlerContext也是一個Dispatcher,它通過Handler向主線程發(fā)送Runnable。

總而言之,Dispatchers.Main中的任務(wù)最后通過其內(nèi)部的HandlerContext dispatch方法使用一個Handler(使用Looper.main)發(fā)送到消息隊列,非常純粹、意料之中的實現(xiàn)。
最后來看一下Dispatchers.Main 在什么情況下會進行dispatch:
默認情況下invokeImmediately為false,并且handler.looper 返回的是Looper.main,所以結(jié)合起來看,isDispatcherNeeded 通常會返回true;如果當當前線程不是主線程時,也會返回true。

?我粗略的看了一下Dispatchers.Default的實現(xiàn),它是使用Java中提供的線程池實現(xiàn)的,具體的邏輯就不在這里展開分析了。

總結(jié):
本文介紹了Continuation,這個在kotlin協(xié)程中非常重要,但是常常委身于幕后的類
通過兩個例子探討了Interceptor和Continuation的關(guān)系,同時也揭示了Dispatcher也是Interceptor,并且通過Interceptor的角度 重新看了一下Dispatchers.Main的源碼。