Android版 kotlin協程入門(二):kotlin協程的關鍵知識點初步講解
由於文章涉及到的只是點比較多、內容可能過長,可以根據自己的能力水平和熟悉程度分階段跳著看。如有講述的不正確的地方勞煩各位私信給筆者,萬分感謝。
kotlin協程的關鍵知識點
上一本章節
《Android kotlin協程入門實戰(一):kotlin協程的基礎用法解讀》末尾我們提到,將在本章節中對以下知識點做初步講解,包含上文提到的
launch
和
async
函式中的3個引數作用。清單如下:
- 協程排程器
CoroutineDispatcher
- 協程下上文
CoroutineContext
作用 - 協程啟動模式
CoroutineStart
- 協程作用域
CoroutineScope
- 掛起函式以及
suspend
關鍵字的作用
當然還有一些其他的知識點也是很重要的,比如:
CoroutineExceptionHandler
、
Continuation
、
Scheduler
、
ContinuationInterceptor
等。但是確實涉及到的東西比較多,如果都展開的話,可能再寫幾個篇幅都沒有辦法講完。上面這些是筆者認為掌握了這些知識點以後,基本可以開始著手專案實戰了。我們後面在實戰的過程中,邊寫邊講解。
協程排程器
上文我們提到一個協程排程器
CoroutineDispatcher
的概念,排程器又是一個什麼神奇的東西。在這裡我們對排程器不做過多深入的解釋,這可是協程的三大件之一,後面我們會有專門的篇幅做深入講解。為了方便我們把協程排程器簡稱為
排程器
,那接下來我們就看看什麼是排程器。偷個懶,引用一下官方的原話:
- 排程器它確定了相關的協程在哪個執行緒或哪些執行緒上執行。協程排程器可以將協程限制在一個特定的執行緒執行,或將它分派到一個執行緒池,亦或是讓它不受限地執行。
對於排程器的實現機制我們已經非常清楚了,官方框架中預置了4個排程器,我們可以透過
Dispatchers
物件直接訪問它們:
public actual object Dispatchers { @JvmStatic public actual val Default: CoroutineDispatcher = createDefaultDispatcher() @JvmStatic public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher @JvmStatic public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined @JvmStatic public val IO: CoroutineDispatcher = DefaultScheduler.IO } 複製程式碼
-
Default
:預設排程器,CPU密集型任務排程器,適合處理後臺計算。通常處理一些單純的計算任務,或者執行時間較短任務。比如:Json的解析,資料計算等 -
IO
:IO排程器,,IO密集型任務排程器,適合執行IO相關操作。比如:網路處理,資料庫操作,檔案操作等 -
Main
:UI排程器, 即在主執行緒上執行,通常用於UI互動,重新整理等 -
Unconfined
:非受限排程器,又或者稱為“無所謂”排程器,不要求協程執行在特定執行緒上。
比如上面我們透過
launch
啟動的時候,因為我們沒有傳入引數,所有實際上它使用的是預設排程器
Dispatchers.Default
GlobalScope.launch{ Log.d("launch", "啟動一個協程") }//等同於GlobalScope.launch(Dispatchers.Default){ Log.d("launch", "啟動一個協程") } 複製程式碼
Dispatchers.IO
和
Dispatchers.Main
就都很好理解了。這是我們以後在Android開發過程中,打交道最多的2個排程器。比如後臺資料上傳,我們就可以使用
Dispatchers.IO
排程器。重新整理介面我們就使用
Dispatchers.Main
排程器。為方便使用官方在Android協程框架庫中,已經為我們定義好了幾個供我們開發使用,如:
MainScope
、
lifecycleScope
、
viewModelScope
。它們都是使用的
Dispatchers.Main
,這些後續我們都將會使用到。
根據我們上面使用的方法,我們好像只有在啟動協程的時候,才能指定具體使用那個
Dispatchers
排程器。如果我要是想中途切換執行緒怎麼辦,比如:
- 現在我們需要透過網路請求獲取到資料的時候填充到我們的佈局當中,但是網路處理在
IO
執行緒上,而重新整理UI是在主執行緒
上,那我們應該怎麼辦。
莫慌,莫慌,萬事萬物總有解決的辦法。官方為我們提供了一個
withContext
頂級函式,使用
withContext
函式來改變協程的上下文,而仍然駐留在相同的協程中,同時
withContext
還攜帶有一個泛型
T
返回值。
public suspend fun <T> withContext( context: CoroutineContext, block: suspend CoroutineScope.() -> T ): T { //...... } 複製程式碼
呀,這一看
withContext
這個東西好像很符合我們的需求嘛,我們可以先使用
launch(Dispatchers.Main)
啟動協程,然後再透過
withContext(Dispatchers.IO)
排程到
IO
執行緒上去做網路請求,把得到的結果返回,這樣我們就解決了我們上面的問題了。
GlobalScope.launch(Dispatchers.Main) { val result = withContext(Dispatchers.IO) { //網路請求... "請求結果" } btn.text = result } 複製程式碼
是不是很簡單!!! 麻麻再也不會說我的handler滿飛了,也不用走那萬惡的回撥地獄了。我想怎麼切就怎麼切,想去走個執行緒就去哪個執行緒。邏輯都按著順序一步一步走,而且程式碼都是這麼的絲滑。還要什麼腳踏車,額.錯了,還要什麼handler,管他回撥不回撥。
協程上下文
CoroutineContext
即協程上下文。它是一個包含了使用者定義的一些各種不同元素的
Element
物件集合。其中主要元素是
Job
、協程排程器
CoroutineDispatcher
、還有包含協程異常
CoroutineExceptionHandler
、攔截器
ContinuationInterceptor
、協程名
CoroutineName
等。這些資料都是和協程密切相關的,每一個
Element
都一個唯一key。
public interface CoroutineContext { public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E? public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R public operator fun plus(context: CoroutineContext): CoroutineContext = if (context === EmptyCoroutineContext) this else context.fold(this) { ...} public fun minusKey(key: Key<*>): CoroutineContext //注意這裡,這個key很關鍵 public interface Key <E : CoroutineContext.Element> public interface Element : CoroutineContext { public val key: Key<*> public override operator fun <E : Element> get(key: Key<E>): E? = if (this.key == key) this as E else null public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = operation(initial, this) public override fun minusKey(key: Key<*>): CoroutineContext = if (this.key == key) EmptyCoroutineContext else this } } 複製程式碼
我們可以看到
Element
是
CoroutineContext
的內部介面,同時它又實現了
CoroutineContext
介面,這麼設計的原因是為了保證
Element
中一定只能存放的
Element
它自己,而不能存放其他型別的資料
CoroutineContext
內還有一個內部介面
Key
,同時它又是
Element
的一個屬性,這個屬性很重要,我們先在這裡插個眼,待會再講解這個屬性的作用。
那我們上面提到
Job
、
CoroutineDispatcher
、
CoroutineExceptionHandler
、
ContinuationInterceptor
、
CoroutineName
等為什麼又可以存放到
CoroutineContext
中呢。我們接著往下看看它們各自的實現:
Job
public interface Job : CoroutineContext.Element { public companion object Key : CoroutineContext.Key<Job> { //省略... } } 複製程式碼
CoroutineDispatcher
public abstract class CoroutineDispatcher : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>( ContinuationInterceptor, { it as? CoroutineDispatcher }) } 複製程式碼
CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element { public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler> } 複製程式碼
ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element { companion object Key : CoroutineContext.Key<ContinuationInterceptor> } 複製程式碼
CoroutineName
public data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) { public companion object Key : CoroutineContext.Key<CoroutineName> } 複製程式碼
現在要開始要集中注意力了。我們可以看到他們都是實現了
Element
介面,同時都有個
CoroutineContext.Key
型別的伴生物件
key
,這個屬性的作用是什麼呢。那我們就得回過頭來看看
CoroutineContext
介面的幾個方法了。
public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): Rpublic operator fun plus(context: CoroutineContext): CoroutineContext = if (context === EmptyCoroutineContext) this else context.fold(this) { ...}public fun minusKey(key: Key<*>): CoroutineContext 複製程式碼
我們先從
plus
方法說起,
plus
有個關鍵字
operator
表示這是一個運算子過載的方法,類似List.plus的運算子,可以透過
+
號來返回一個包含原始集合和第二個運算元中的元素的結果。同理
CoroutineContext
中是透過
plus
來返回一個由原始的
Element
集合和透過
+
號引入的
Element
產生新的
Element
集合。
get
方法,顧名思義。可以透過
key
來獲取一個
Element
fold
方法它和集合中的fold是一樣的,用來遍歷當前協程上下文中的
Element
集合。
minusKey
方法
plus
作用相反,它相當於是做減法,是用來取出除
key
以外的當前協程上下文其他
Element
,返回的就是不包含
key
的協程上下文。
現在我們就知道為什麼我們之前說
Element
中的
key
這個屬性很重要了吧。因為我們就是透過它從協程上下文中獲取我們想要的
Element
,同時也解釋為什麼
Job
、
CoroutineDispatcher
、
CoroutineExceptionHandler
、
ContinuationInterceptor
、
CoroutineName
等等,這些
Element
都有需要有一個
CoroutineContext.Key
型別的伴生物件
key
。我們寫個測試方法: 如:
private fun testCoroutineContext(){ val coroutineContext1 = Job() + CoroutineName("這是第一個上下文") Log.d("coroutineContext1", "$coroutineContext1") val coroutineContext2 = coroutineContext1 + Dispatchers.Default + CoroutineName("這是第二個上下文") Log.d("coroutineContext2", "$coroutineContext2") val coroutineContext3 = coroutineContext2 + Dispatchers.Main + CoroutineName("這是第三個上下文") Log.d("coroutineContext3", "$coroutineContext3") } 複製程式碼
D/coroutineContext1: [JobImpl{Active}@21a6a21, CoroutineName(這是第一個上下文)] D/coroutineContext2: [JobImpl{Active}@21a6a21, CoroutineName(這是第二個上下文), Dispatchers.Default] D/coroutineContext3: [JobImpl{Active}@21a6a21, CoroutineName(這是第三個上下文), Dispatchers.Main] 複製程式碼
我們透過對比日誌輸出資訊可以看到,透過
+
號我們可以把多個
Element
整合到一個集合中,同時我們也發現:
- 三個上下文中的
Job
是同一個物件。 - 第二個上下文在第一個的基礎上增加了一個新的
CoroutineName
,新增的CoroutineName
替換了第一個上下文中的CoroutineName
。 - 第三個上下文在第二個的基礎上又增加了一個新的
CoroutineName
和Dispatchers
,同時他們也替換了第二個上下文中的CoroutineName
和Dispatchers
。
但是因為這個
+
運算子是不對稱的,所以在我們實際的運用過程中,透過
+
增加
Element
的時候一定要注意它們結合的順序。那麼現在關於協程上下文的內容就講到這裡,我們點到為止,後面在深入理解階段在細講這些東西執行的原理細節。
協程啟動模式
CoroutineStart
協程啟動模式,是啟動協程時需要傳入的第二個引數。協程啟動有4種:
-
DEFAULT
預設啟動模式,我們可以稱之為餓漢啟動模式,因為協程建立後立即開始排程,雖然是立即排程,單不是立即執行,有可能在執行前被取消。 -
LAZY
懶漢啟動模式,啟動後並不會有任何排程行為,直到我們需要它執行的時候才會產生排程。也就是說只有我們主動的呼叫Job
的start
、join
或者await
等函式時才會開始排程。 -
ATOMIC
一樣也是在協程建立後立即開始排程,但是它和DEFAULT
模式有一點不一樣,透過ATOMIC
模式啟動的協程執行到第一個掛起點之前是不響應cancel
取消操作的,ATOMIC
一定要涉及到協程掛起後cancel
取消操作的時候才有意義。 -
UNDISPATCHED
協程在這種模式下會直接開始在當前執行緒下執行,直到執行到第一個掛起點。這聽起來有點像ATOMIC
,不同之處在於UNDISPATCHED
是不經過任何排程器就開始執行的。當然遇到掛起點之後的執行,將取決於掛起點本身的邏輯和協程上下文中的排程器。
我們可以透過一個小例子的來看看這幾個啟動模式的實際情況:
private fun testCoroutineStart(){ val defaultJob = GlobalScope.launch{ Log.d("defaultJob", "CoroutineStart.DEFAULT") } defaultJob.cancel() val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){ Log.d("lazyJob", "CoroutineStart.LAZY") } val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC){ Log.d("atomicJob", "CoroutineStart.ATOMIC掛起前") delay(100) Log.d("atomicJob", "CoroutineStart.ATOMIC掛起後") } atomicJob.cancel() val undispatchedJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){ Log.d("undispatchedJob", "CoroutineStart.UNDISPATCHED掛起前") delay(100) Log.d("atomicJob", "CoroutineStart.UNDISPATCHED掛起後") } undispatchedJob.cancel() } 複製程式碼
每個模式我們分別啟動一個一次,
DEFAULT
模式啟動時,我們接著呼叫了
cancel
取消協程,
ATOMIC
模式啟動時,我們在裡面增加了一個掛起點
delay
掛起函式,來區分
ATOMIC
啟動時的掛起前後執行情況,同樣的
UNDISPATCHED
模式啟動時,我們也呼叫了
cancel
取消協程,我們看實際的日誌輸出情況:
D/defaultJob: CoroutineStart.DEFAULTD/atomicJob: CoroutineStart.ATOMIC掛起前 D/undispatchedJob: CoroutineStart.UNDISPATCHED掛起前 複製程式碼
或者
D/undispatchedJob: CoroutineStart.UNDISPATCHED掛起前 D/atomicJob: CoroutineStart.ATOMIC掛起前 複製程式碼
為什麼會出現2種情況。我們上面提到過
DEFAULT
模式協程建立後立即開始排程,但不是立即執行,所有它有可能會被
cancel
取消,導致沒有輸出
defaultJob
這條日誌。
同樣的
ATOMIC
模式啟動的時候也接著呼叫了
cancel
取消協程,但是因為沒有遇到掛起點,所以掛起前的日誌輸出了,但是掛起後的日誌沒有輸出。
而
UNDISPATCHED
模式啟動的時候也接著呼叫了
cancel
取消協程,同樣的因為沒有遇到掛起點所以輸出了
UNDISPATCHED掛起前
,但是因為
UNDISPATCHED
是立即執行的,所以他的日誌
UNDISPATCHED掛起前
輸出在
ATOMIC掛起前
的前面。
接著我們在補充一下關於
UNDISPATCHED
模式。我們上面有提到當以
UNDISPATCHED
模式啟動時,遇到掛起點之後的執行,將取決於掛起點本身的邏輯和協程上下文中的排程器。這句話我們又要怎麼理解呢。我們還是以一個例子來認識解釋
UNDISPATCHED
模式,比如:
private fun testUnDispatched(){ GlobalScope.launch(Dispatchers.Main){ val job = launch(Dispatchers.IO) { Log.d("${Thread.currentThread().name}執行緒", "-> 掛起前") delay(100) Log.d("${Thread.currentThread().name}執行緒", "-> 掛起後") } Log.d("${Thread.currentThread().name}執行緒", "-> join前") job.join() Log.d("${Thread.currentThread().name}執行緒", "-> join後") } } 複製程式碼
那我們將會看到如下輸出,掛起前後都在一個
worker-1
執行緒裡面執行:
D/main執行緒: -> join前 D/DefaultDispatcher-worker-1執行緒: -> 掛起前 D/DefaultDispatcher-worker-1執行緒: -> 掛起後 D/main執行緒: -> join後 複製程式碼
現在我們在稍作修改,我們在子協程
launch
的時候使用
UNDISPATCHED
模式啟動:
private fun testUnDispatched(){ GlobalScope.launch(Dispatchers.Main){ val job = launch(Dispatchers.IO,start = CoroutineStart.UNDISPATCHED) { Log.d("${Thread.currentThread().name}執行緒", "-> 掛起前") delay(100) Log.d("${Thread.currentThread().name}執行緒", "-> 掛起後") } Log.d("${Thread.currentThread().name}執行緒", "-> join前") job.join() Log.d("${Thread.currentThread().name}執行緒", "-> join後") } } 複製程式碼
那我們將會看到如下輸出:
D/main執行緒: -> 掛起前 D/main執行緒: -> join前 D/DefaultDispatcher-worker-1執行緒: -> 掛起後 D/main執行緒: -> join後 複製程式碼
我們看到當以
UNDISPATCHED
模式即使我們指定了協程排程器
Dispatchers.IO
,
掛起前
還是在
main
執行緒裡執行,但是
掛起後
是在
worker-1
執行緒裡面執行,這是因為當以
UNDISPATCHED
啟動時,協程在這種模式下會直接開始在當前執行緒下執行,直到第一個掛起點。遇到掛起點之後的執行,將取決於掛起點本身的邏輯和協程上下文中的排程器,即
join
處恢復執行時,因為所在的協程有排程器,所以後面的執行將會在排程器對應的執行緒上執行。
我們再改一下,把子協程在
launch
的時候使用
UNDISPATCHED
模式啟動,去掉
Dispatchers.IO
排程器,那又會出現什麼情況呢
private fun testUnDispatched(){ GlobalScope.launch(Dispatchers.Main){ val job = launch(start = CoroutineStart.UNDISPATCHED) { Log.d("${Thread.currentThread().name}執行緒", "-> 掛起前") delay(100) Log.d("${Thread.currentThread().name}執行緒", "-> 掛起後") } Log.d("${Thread.currentThread().name}執行緒", "-> join前") job.join() Log.d("${Thread.currentThread().name}執行緒", "-> join後") } } 複製程式碼
D/main執行緒: -> 掛起前 D/main執行緒: -> join前 D/main執行緒: -> 掛起後 D/main執行緒: -> join後 複製程式碼
我們發現它們都在一個執行緒裡面執行了。這是因為當透過
UNDISPATCHED
啟動後遇到掛起,
join
處恢復執行時,如果所在的協程沒有指定排程器,那麼就會在
join
處恢復執行的執行緒裡執行,即
掛起後
是在父協程
(Dispatchers.Main
執行緒裡面執行,而最後
join後
這條日誌的輸出排程取決於這個最外層的協程的排程規則。
現在我們可以總結一下,當以
UNDISPATCHED
啟動時:
-
無論我們是否指定協程排程器,
掛起前
的執行都是在當前執行緒下執行。 -
如果所在的協程沒有指定排程器,那麼就會在
join
處恢復執行的執行緒裡執行,即我們上述案例中的掛起後
的執行是在main
執行緒中執行。 -
當我們指定了協程排程器時,遇到掛起點之後的執行將取決於掛起點本身的邏輯和協程上下文中的排程器。即
join
處恢復執行時,因為所在的協程有排程器,所以後面的執行將會在排程器對應的執行緒上執行。
同樣的我們點到為止,關於啟動模式的的相關內容我們就現講到這裡。
協程作用域
協程作用域
CoroutineScope
為協程定義作用範圍,每個協程生成器
launch
、
async
等都是
CoroutineScope
的擴充套件,並繼承了它的
coroutineContext
自動傳播其所有
Element
和取消。協程作用域本質是一個介面,不建議手工實現該介面,而應該首選委託實現。下面我們列出了部分
CoroutineScope
相關定義:
public interface CoroutineScope { public val coroutineContext: CoroutineContext }public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope = ContextScope(coroutineContext + context)public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)public object GlobalScope : CoroutineScope { override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext }public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job()) 複製程式碼
我們可以看到
CoroutineScope
也過載了
plus
方法,透過
+
號來新增或者修改我們
CoroutineContext
協程上下文中的
Element
。同時官方也為我們定義好了
MainScope
和
GlobalScope
2個頂級作用域。
GlobalScope
我們已經很熟了,前面的案例都是透過它來實現的。
MainScope
我們可以看到它的上下文是透過
SupervisorJob
和
Dispatchers.Main
組合的,說明它是一個在主執行緒執行的協程作用域,我們在後續的Android實戰開發中,會結合Activity、Fragment,dialog等使用它。這裡不再繼續往下擴充套件。
至於
SupervisorJob
分析它之前,我們得先說一下協程作用域的分類。我們之前提到過父協程和子協程的概念,既然有父協程和子協程,那麼必然也有父協程作用域和子父協程作用域。不過我們不是這麼稱呼,因為他們不僅僅是父與子的概念。協程作用域分為三種:
-
頂級作用域
--> 沒有父協程的協程所在的作用域稱之為頂級作用域。 -
協同作用域
--> 在協程中啟動一個協程,新協程為所在協程的子協程。子協程所在的作用域預設為協同作用域。此時子協程丟擲未捕獲的異常時,會將異常傳遞給父協程處理,如果父協程被取消,則所有子協程同時也會被取消。 -
主從作用域
官方稱之為監督作用域
。與協同作用域一致,區別在於該作用域下的協程取消操作的單向傳播性,子協程的異常不會導致其它子協程取消。但是如果父協程被取消,則所有子協程同時也會被取消。
同時補充一點:父協程需要等待所有的子協程執行完畢之後才會進入
Completed
狀態,不管父協程自身的協程體是否已經執行完成。我們在最開始提到協程生命週期的時候就提到過下,現在回過頭看是不是感覺很流程變得清晰。
wait children +-----+ start +--------+ complete +-------------+ finish +-----------+| New | -----> | Active | ---------> | Completing | -------> | Completed |+-----+ +--------+ +-------------+ +-----------+ | cancel / fail | | +----------------+ | | V V +------------+ finish +-----------+ | Cancelling | --------------------------------> | Cancelled | +------------+ +-----------+ 複製程式碼
子協程會繼承父協程的協程上下文中的
Element
,如果自身有相同key的成員,則覆蓋對應的
key
,覆蓋的效果僅限自身範圍內有效。這個就可以用上我們前面學到的協程上下文
CoroutineContext
的知識,小案例奉上:
private fun testCoroutineScope(){ GlobalScope.launch(Dispatchers.Main){ Log.d("父協程上下文", "$coroutineContext") launch(CoroutineName("第一個子協程")) { Log.d("第一個子協程上下文", "$coroutineContext") } launch(Dispatchers.Unconfined) { Log.d("第二個子協程協程上下文", "$coroutineContext") } } } 複製程式碼
日誌順序的問題我們前面已經分析過原因,如果還不懂的話,麻煩您回到基礎用法裡面仔細的再看一遍。
D/父協程上下文: [StandaloneCoroutine{Active}@81b6e46, Dispatchers.Main] D/第二個子協程協程上下文: [StandaloneCoroutine{Active}@f6b7807, Dispatchers.Unconfined] D/第一個子協程上下文: [CoroutineName(第一個子協程), StandaloneCoroutine{Active}@bbe6d34, Dispatchers.Main] 複製程式碼
可以看到第一個子協程的覆蓋了父協程的
Job
,但是它繼承了父協程的排程器
Dispatchers.Main
,同時也新增了一個
CoroutineName
。第二個子協程覆蓋了父協程的
Job
,也將父協程的排程器覆蓋為
Unconfined
,但是他沒有繼承第一個子協程的
CoroutineName
,這就是我們說的覆蓋的效果僅限自身範圍內有效。接下來我們看看上面提到的
協同作用域
和
主從(監督)作用域
異常傳遞和協程取消的問題。
我們上面提到
協同作用域
如果子協程丟擲未捕獲的異常時,會將異常傳遞給父協程處理,如果父協程被取消,則所有子協程同時也會被取消。先上程式碼看看效果:
private fun testCoroutineScope2() { val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable") } GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) { Log.d("scope", "--------- 1") launch(CoroutineName("scope2") + exceptionHandler) { Log.d("scope", "--------- 2") throw NullPointerException("空指標") Log.d("scope", "--------- 3") } val scope3 = launch(CoroutineName("scope3") + exceptionHandler) { Log.d("scope", "--------- 4") delay(2000) Log.d("scope", "--------- 5") } scope3.join() Log.d("scope", "--------- 6") } } 複製程式碼
D/scope: --------- 1 D/scope: --------- 2 D/exceptionHandler: CoroutineName(scope1) java.lang.NullPointerException: 空指標 複製程式碼
可以看到子協程
scope2
丟擲了一個異常,將異常傳遞給父協程
scope1
處理,但是因為任何一個子協程異常退出會導致整體都將退出。所以導致父協程
scope1
未執行完成成就被取消,同時還未執行完子協程
scope3
也被取消了。
主從(監督)作用域
與
協同作用域
一致,區別在於該作用域下的協程取消操作的單向傳播性,子協程的異常不會導致其它子協程取消。分析
主從(監督)作用域
的時候,我們需要用到
supervisorScope
或者
SupervisorJob
,如下程式碼塊:
private fun testCoroutineScope3() { val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable") } GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) { supervisorScope { Log.d("scope", "--------- 1") launch(CoroutineName("scope2")) { Log.d("scope", "--------- 2") throw NullPointerException("空指標") Log.d("scope", "--------- 3") val scope3 = launch(CoroutineName("scope3")) { Log.d("scope", "--------- 4") delay(2000) Log.d("scope", "--------- 5") } scope3.join() } val scope4 = launch(CoroutineName("scope4")) { Log.d("scope", "--------- 6") delay(2000) Log.d("scope", "--------- 7") } scope4.join() Log.d("scope", "--------- 8") } } } 複製程式碼
D/scope: --------- 1 D/scope: --------- 2 D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指標 D/scope: --------- 6 D/scope: --------- 7 D/scope: --------- 8 複製程式碼
可以看到子協程
scope2
丟擲了一個異常,並將異常傳遞給父協程
scope1
處理,同時也結束了自己本身。因為在於
主從(監督)作用域
下的協程取消操作是單向傳播性,因此協程
scope2
的異常並沒有導致父協程退出,所以
6
7
8
都照常輸出,而
3
4
5
因為在協程
scope2
裡面所以沒有輸出。
我們剛剛使用了
supervisorScope
實現了
主從(監督)作用域
,那我們透過
SupervisorJob
又該如何實現呢。我們把
supervisorScope
稱之為
主從(監督)作用域
,那麼
SupervisorJob
就可以稱之為
主從(監督)作業
,如下:
private fun testCoroutineScope4() { val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable -> Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable") } val coroutineScope = CoroutineScope(SupervisorJob() +CoroutineName("coroutineScope")) GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) { with(coroutineScope){ val scope2 = launch(CoroutineName("scope2") + exceptionHandler) { Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}") throw NullPointerException("空指標") } val scope3 = launch(CoroutineName("scope3") + exceptionHandler) { scope2.join() Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}") delay(2000) Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}") } scope2.join() Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}") coroutineScope.cancel() scope3.join() Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}") } Log.d("scope", "6--------- ${coroutineContext[CoroutineName]}") } } 複製程式碼
D/scope: 1--------- CoroutineName(scope2) D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指標 D/scope: 2--------- CoroutineName(scope3) D/scope: 4--------- CoroutineName(coroutineScope) D/scope: 5--------- CoroutineName(coroutineScope) D/scope: 6--------- CoroutineName(scope1) 複製程式碼
是不是感覺和
supervisorScope
的用法很像,我們透過建立了一個
SupervisorJob
的主從(監督)協程作用域,呼叫了子協程的
join
是為了保證它一定是會執行。同樣的子協程
scope2
丟擲了一個異常,透過協程
scope2
自己內部消化了,同時也結束了自己本身。
因為協程
scope2
的異常並沒有導致
coroutineScope
作用域下的協程取消退出,所以協程
scope3
照常執行輸出
2
,後又因為呼叫了我們定義的協程作用域
coroutineScope
的
cancel
方法取消了協程,所以即使我們後面呼叫了協程
scope3
的
join
,也沒有輸出
3
,因為
SupervisorJob
的取消是向下傳播的,所以後面的
4
5
都是在
coroutineScope
的作用域中輸出的。
現在我們關於協程作用域
CoroutineScope
的作用我們已經有了一個大概的瞭解,同樣的因為這個篇幅中我們是基礎講解,所以我們點到為止,如果還想深入瞭解,那就只能看後面的深入協程篇幅。
掛起函式
透過前面的篇幅我們已經知道,使用
suspend
關鍵字修飾的函式叫作
掛起函式
,
掛起函式
只能在協程體內,或著在其他
掛起函式
內呼叫。那掛起又是啥玩意呢。
我估計各位看到這裡的時候,可能有些人已經被上面的知識點弄的有點暈乎,別急,先放鬆下大腦,喝杯水,然後做個眼保健操緩解一下。下面開始敲黑板了,打起精神,要開始劃重點了。
首先一個
掛起函式
既然要掛起,那麼他必定得有一個
掛起點
,不然我們怎麼知道函式是否掛起,從哪掛起呢。 我們定義一個空實現的
suspend
方法,然後透過AS的工具欄中
Tools
->
kotlin
->
show kotlin ByteCode
解析成位元組碼
private suspend fun test(){ } 複製程式碼
final synthetic test(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 複製程式碼
public interface Continuation<in T> { public val context: CoroutineContext public fun resumeWith(result: Result<T>)} 複製程式碼
我們看到
test
方法需要的是一個
Continuation
介面,官方給的介紹是用於掛起點之後,返回型別為
T
的值用的。那我們又是怎麼拿到的這個
Continuation
呢。要解開這個問題我們得先回到協程的建立和執行是的過程。
我們啟動一個協程無非是透過
launch
,
async
等方法。我們之前有說到過他們的啟動模式
CoroutineStart
,但是並沒有深入的去分析它的建立和啟動過程,我們這裡先回過頭大概的看一下:
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { val newContext = newCoroutineContext(context) val coroutine = if (start.isLazy) LazyStandaloneCoroutine(newContext, block) else StandaloneCoroutine(newContext, active = true) coroutine.start(start, coroutine, block) return coroutine } 複製程式碼
我們看到在透過
launch
啟動一個協程的時候,他透過
coroutine
的
start
方法啟動協程,然後我們接著往下看
public fun start(start: CoroutineStart, block: suspend () -> T) { initParentJob() start(block, this) }public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) { initParentJob() start(block, receiver, this) } 複製程式碼
然後
start
方法裡面呼叫了
CoroutineStart
的
invoke
,這個時候我們發現了
Continuation
。
public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit = when (this) { DEFAULT -> block.startCoroutineCancellable(completion) ATOMIC -> block.startCoroutine(completion) UNDISPATCHED -> block.startCoroutineUndispatched(completion) LAZY -> Unit // will start lazily }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 } 複製程式碼
而
Continuation
又是透過
start
方法傳進來的
coroutine
。所以現在可以確定,我們的協程體本身就是一個
Continuation
,這也就解釋了為什麼可以在協程體內呼叫
suspend
掛起函式了。
現在我們也可以確定,在協程內部
掛起函式
的呼叫處就是
掛起點
,如果
掛起點
出現非同步呼叫,那麼當前協程就被掛起,直到對應的
Continuation
透過呼叫
resumeWith
函式才會恢復協程的執行,同時返回
Result<T>
型別的成功或者失敗的結果。
由於章節主題的限制,這裡我們就不再下深入了。需要注意的是
掛起函式
不一定真的會掛起,如果只是提供了掛起的條件,但是協程沒有產生非同步呼叫,那麼協程還是不會被掛起。
預告:下一篇我們將會講解kotlin協程中的異常處理,其實我們在這篇章節中已經,提到了一些異常處理,沒有注意的同學可以回到
協程作用域
看看。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2795354/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android版kotlin協程入門(四):kotlin協程開發實戰AndroidKotlin
- Android版kotlin協程入門(三):kotlin協程的異常處理AndroidKotlin
- Android Kotlin協程入門AndroidKotlin
- Android入門教程 | Kotlin協程入門AndroidKotlin
- Kotlin Coroutine(協程): 二、初識協程Kotlin
- Kotlin Coroutines(協程)講解Kotlin
- Kotlin協程快速入門Kotlin
- 在 Android 開發中使用 Kotlin 協程 (一) -- 初識 Kotlin 協程AndroidKotlin
- Kotlin Coroutine(協程) 基本知識Kotlin
- Android Kotlin 協程初探AndroidKotlin
- Kotlin Coroutine(協程): 三、瞭解協程Kotlin
- 【Kotlin】協程Kotlin
- Kotlin(android)協程中文翻譯KotlinAndroid
- Kotlin 協程一 —— CoroutineKotlin
- kotlin協程的掛起suspendKotlin
- Kotlin Coroutine(協程)簡介Kotlin
- Kotlin協程快速進階Kotlin
- Kotlin協程學習之路【一】Kotlin
- Kotlin coroutine之協程基礎Kotlin
- 【譯】kotlin 協程官方文件(1)-協程基礎(Coroutine Basics)Kotlin
- 扒一扒Kotlin協程的底褲Kotlin
- 揭開Kotlin協程的神秘面紗Kotlin
- Kotlin 1.4.0-RC協程除錯Kotlin除錯
- 【譯】使用kotlin協程提高app效能KotlinAPP
- [譯] Kotlin 協程高階使用技巧Kotlin
- 【譯】第一次走進 Android 中的 Kotlin 協程AndroidKotlin
- 【譯】kotlin 協程官方文件(6)-通道(Channels)Kotlin
- 真香!Kotlin+MVVM+LiveData+協程 打造 Wanandroid!KotlinMVVMLiveDataNaNAndroid
- 一篇文章帶你瞭解——Kotlin協程Kotlin
- 【思貨】kotlin協程優雅的與Retrofit纏綿-kotlin DSL簡述Kotlin
- [譯] 使用 Kotlin 協程改進應用效能Kotlin
- kotlin中將回撥改寫為協程Kotlin
- 資源混淆是如何影響到Kotlin協程的Kotlin
- rxjava回撥地獄-kotlin協程來幫忙RxJavaKotlin
- 協程庫基礎知識
- 【思貨】kotlin協程優雅的與Retrofit纏綿-正文Kotlin
- 忘記Rxjava吧,你應該試試Kotlin的協程RxJavaKotlin
- Android技術分享| 利用Kotlin協程,多工並行,測試RTM SDK效能AndroidKotlin並行