由于文章涉及到的只是點(diǎn)比較多胞谭、內(nèi)容可能過長(zhǎng)垃杖,可以根據(jù)自己的能力水平和熟悉程度分階段跳著看。如有講述的不正確的地方勞煩各位私信給筆者丈屹,萬分感謝调俘。
kotlin協(xié)程的關(guān)鍵知識(shí)點(diǎn)
上一本章節(jié)《Android kotlin協(xié)程入門實(shí)戰(zhàn)(一):kotlin協(xié)程的基礎(chǔ)用法解讀》末尾我們提到,將在本章節(jié)中對(duì)以下知識(shí)點(diǎn)做初步講解旺垒,包含上文提到的launch
和async
函數(shù)中的3個(gè)參數(shù)作用彩库。清單如下:
- 協(xié)程調(diào)度器
CoroutineDispatcher
- 協(xié)程下上文
CoroutineContext
作用 - 協(xié)程啟動(dòng)模式
CoroutineStart
- 協(xié)程作用域
CoroutineScope
- 掛起函數(shù)以及
suspend
關(guān)鍵字的作用
當(dāng)然還有一些其他的知識(shí)點(diǎn)也是很重要的,比如:CoroutineExceptionHandler
先蒋、Continuation
骇钦、Scheduler
、ContinuationInterceptor
等竞漾。但是確實(shí)涉及到的東西比較多眯搭,如果都展開的話,可能再寫幾個(gè)篇幅都沒有辦法講完业岁。上面這些是筆者認(rèn)為掌握了這些知識(shí)點(diǎn)以后鳞仙,基本可以開始著手項(xiàng)目實(shí)戰(zhàn)了。我們后面在實(shí)戰(zhàn)的過程中笔时,邊寫邊講解棍好。
協(xié)程調(diào)度器
上文我們提到一個(gè)協(xié)程調(diào)度器CoroutineDispatcher
的概念,調(diào)度器又是一個(gè)什么神奇的東西。在這里我們對(duì)調(diào)度器不做過多深入的解釋借笙,這可是協(xié)程的三大件之一扒怖,后面我們會(huì)有專門的篇幅做深入講解。為了方便我們把協(xié)程調(diào)度器簡(jiǎn)稱為調(diào)度器
业稼,那接下來我們就看看什么是調(diào)度器盗痒。偷個(gè)懶,引用一下官方的原話:
- 調(diào)度器它確定了相關(guān)的協(xié)程在哪個(gè)線程或哪些線程上執(zhí)行盼忌。協(xié)程調(diào)度器可以將協(xié)程限制在一個(gè)特定的線程執(zhí)行,或?qū)⑺峙傻揭粋€(gè)線程池掂墓,亦或是讓它不受限地運(yùn)行谦纱。
對(duì)于調(diào)度器的實(shí)現(xiàn)機(jī)制我們已經(jīng)非常清楚了,官方框架中預(yù)置了4個(gè)調(diào)度器君编,我們可以通過Dispatchers
對(duì)象直接訪問它們:
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
}
復(fù)制代碼
-
Default
:默認(rèn)調(diào)度器跨嘉,CPU密集型任務(wù)調(diào)度器,適合處理后臺(tái)計(jì)算吃嘿。通常處理一些單純的計(jì)算任務(wù)祠乃,或者執(zhí)行時(shí)間較短任務(wù)。比如:Json的解析兑燥,數(shù)據(jù)計(jì)算等 -
IO
:IO調(diào)度器亮瓷,,IO密集型任務(wù)調(diào)度器降瞳,適合執(zhí)行IO相關(guān)操作嘱支。比如:網(wǎng)絡(luò)處理,數(shù)據(jù)庫操作挣饥,文件操作等 -
Main
:UI調(diào)度器除师, 即在主線程上執(zhí)行,通常用于UI交互扔枫,刷新等 -
Unconfined
:非受限調(diào)度器汛聚,又或者稱為“無所謂”調(diào)度器,不要求協(xié)程執(zhí)行在特定線程上短荐。
比如上面我們通過launch
啟動(dòng)的時(shí)候倚舀,因?yàn)槲覀儧]有傳入?yún)?shù),所有實(shí)際上它使用的是默認(rèn)調(diào)度器Dispatchers.Default
GlobalScope.launch{
Log.d("launch", "啟動(dòng)一個(gè)協(xié)程")
}
//等同于
GlobalScope.launch(Dispatchers.Default){
Log.d("launch", "啟動(dòng)一個(gè)協(xié)程")
}
復(fù)制代碼
Dispatchers.IO
和Dispatchers.Main
就都很好理解了忍宋。這是我們以后在Android開發(fā)過程中瞄桨,打交道最多的2個(gè)調(diào)度器。比如后臺(tái)數(shù)據(jù)上傳讶踪,我們就可以使用Dispatchers.IO
調(diào)度器芯侥。刷新界面我們就使用Dispatchers.Main
調(diào)度器。為方便使用官方在Android協(xié)程框架庫中,已經(jīng)為我們定義好了幾個(gè)供我們開發(fā)使用柱查,如:MainScope
廓俭、lifecycleScope
、viewModelScope
唉工。它們都是使用的Dispatchers.Main
研乒,這些后續(xù)我們都將會(huì)使用到。
根據(jù)我們上面使用的方法淋硝,我們好像只有在啟動(dòng)協(xié)程的時(shí)候雹熬,才能指定具體使用那個(gè)Dispatchers
調(diào)度器。如果我要是想中途切換線程怎么辦谣膳,比如:
- 現(xiàn)在我們需要通過網(wǎng)絡(luò)請(qǐng)求獲取到數(shù)據(jù)的時(shí)候填充到我們的布局當(dāng)中竿报,但是網(wǎng)絡(luò)處理在
IO
線程上,而刷新UI是在主線程
上继谚,那我們應(yīng)該怎么辦烈菌。
莫慌,莫慌花履,萬事萬物總有解決的辦法芽世。官方為我們提供了一個(gè)withContext
頂級(jí)函數(shù),使用withContext
函數(shù)來改變協(xié)程的上下文诡壁,而仍然駐留在相同的協(xié)程中济瓢,同時(shí)withContext
還攜帶有一個(gè)泛型T
返回值。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
//......
}
復(fù)制代碼
呀妹卿,這一看withContext
這個(gè)東西好像很符合我們的需求嘛葬荷,我們可以先使用launch(Dispatchers.Main)
啟動(dòng)協(xié)程,然后再通過withContext(Dispatchers.IO)
調(diào)度到IO
線程上去做網(wǎng)絡(luò)請(qǐng)求纽帖,把得到的結(jié)果返回,這樣我們就解決了我們上面的問題了宠漩。
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) {
//網(wǎng)絡(luò)請(qǐng)求...
"請(qǐng)求結(jié)果"
}
btn.text = result
}
復(fù)制代碼
是不是很簡(jiǎn)單!0弥薄扒吁! 麻麻再也不會(huì)說我的handler滿飛了,也不用走那萬惡的回調(diào)地獄了室囊。我想怎么切就怎么切雕崩,想去走個(gè)線程就去哪個(gè)線程。邏輯都按著順序一步一步走融撞,而且代碼都是這么的絲滑盼铁。還要什么自行車,額.錯(cuò)了尝偎,還要什么handler饶火,管他回調(diào)不回調(diào)鹏控。
協(xié)程上下文
CoroutineContext
即協(xié)程上下文。它是一個(gè)包含了用戶定義的一些各種不同元素的Element
對(duì)象集合。其中主要元素是Job
、協(xié)程調(diào)度器CoroutineDispatcher
覆旱、還有包含協(xié)程異常CoroutineExceptionHandler
、攔截器ContinuationInterceptor
砸逊、協(xié)程名CoroutineName
等。這些數(shù)據(jù)都是和協(xié)程密切相關(guān)的,每一個(gè)Element
都一個(gè)唯一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
//注意這里找筝,這個(gè)key很關(guān)鍵
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
}
}
復(fù)制代碼
我們可以看到Element
是CoroutineContext
的內(nèi)部接口,同時(shí)它又實(shí)現(xiàn)了CoroutineContext
接口慷吊,這么設(shè)計(jì)的原因是為了保證Element
中一定只能存放的Element
它自己袖裕,而不能存放其他類型的數(shù)據(jù)CoroutineContext
內(nèi)還有一個(gè)內(nèi)部接口Key
,同時(shí)它又是Element
的一個(gè)屬性罢浇,這個(gè)屬性很重要陆赋,我們先在這里插個(gè)眼沐祷,待會(huì)再講解這個(gè)屬性的作用嚷闭。
那我們上面提到Job
、CoroutineDispatcher
赖临、CoroutineExceptionHandler
胞锰、ContinuationInterceptor
、CoroutineName
等為什么又可以存放到CoroutineContext
中呢兢榨。我們接著往下看看它們各自的實(shí)現(xiàn):
Job
public interface Job : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<Job> {
//省略...
}
}
復(fù)制代碼
CoroutineDispatcher
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
}
復(fù)制代碼
CoroutineExceptionHandler
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
}
復(fù)制代碼
ContinuationInterceptor
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
復(fù)制代碼
CoroutineName
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
public companion object Key : CoroutineContext.Key<CoroutineName>
}
復(fù)制代碼
現(xiàn)在要開始要集中注意力了嗅榕。我們可以看到他們都是實(shí)現(xiàn)了Element
接口,同時(shí)都有個(gè)CoroutineContext.Key
類型的伴生對(duì)象key
,這個(gè)屬性的作用是什么呢吵聪。那我們就得回過頭來看看CoroutineContext
接口的幾個(gè)方法了凌那。
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
復(fù)制代碼
我們先從plus
方法說起,plus
有個(gè)關(guān)鍵字operator
表示這是一個(gè)運(yùn)算符重載的方法吟逝,類似List.plus的運(yùn)算符帽蝶,可以通過+
號(hào)來返回一個(gè)包含原始集合和第二個(gè)操作數(shù)中的元素的結(jié)果。同理CoroutineContext
中是通過plus
來返回一個(gè)由原始的Element
集合和通過+
號(hào)引入的Element
產(chǎn)生新的Element
集合块攒。
get
方法励稳,顧名思義〈丫可以通過 key
來獲取一個(gè)Element
fold
方法它和集合中的fold是一樣的驹尼,用來遍歷當(dāng)前協(xié)程上下文中的Element
集合。
minusKey
方法plus
作用相反庞呕,它相當(dāng)于是做減法,是用來取出除key
以外的當(dāng)前協(xié)程上下文其他Element
新翎,返回的就是不包含key
的協(xié)程上下文。
現(xiàn)在我們就知道為什么我們之前說Element
中的key
這個(gè)屬性很重要了吧。因?yàn)槲覀兙褪峭ㄟ^它從協(xié)程上下文中獲取我們想要的Element
料祠,同時(shí)也解釋為什么Job
骆捧、CoroutineDispatcher
、CoroutineExceptionHandler
髓绽、ContinuationInterceptor
敛苇、CoroutineName
等等,這些Element
都有需要有一個(gè)CoroutineContext.Key
類型的伴生對(duì)象key
顺呕。我們寫個(gè)測(cè)試方法: 如:
private fun testCoroutineContext(){
val coroutineContext1 = Job() + CoroutineName("這是第一個(gè)上下文")
Log.d("coroutineContext1", "$coroutineContext1")
val coroutineContext2 = coroutineContext1 + Dispatchers.Default + CoroutineName("這是第二個(gè)上下文")
Log.d("coroutineContext2", "$coroutineContext2")
val coroutineContext3 = coroutineContext2 + Dispatchers.Main + CoroutineName("這是第三個(gè)上下文")
Log.d("coroutineContext3", "$coroutineContext3")
}
復(fù)制代碼
D/coroutineContext1: [JobImpl{Active}@21a6a21, CoroutineName(這是第一個(gè)上下文)]
D/coroutineContext2: [JobImpl{Active}@21a6a21, CoroutineName(這是第二個(gè)上下文), Dispatchers.Default]
D/coroutineContext3: [JobImpl{Active}@21a6a21, CoroutineName(這是第三個(gè)上下文), Dispatchers.Main]
復(fù)制代碼
我們通過對(duì)比日志輸出信息可以看到枫攀,通過+
號(hào)我們可以把多個(gè)Element
整合到一個(gè)集合中,同時(shí)我們也發(fā)現(xiàn):
- 三個(gè)上下文中的
Job
是同一個(gè)對(duì)象株茶。 - 第二個(gè)上下文在第一個(gè)的基礎(chǔ)上增加了一個(gè)新的
CoroutineName
,新增的CoroutineName
替換了第一個(gè)上下文中的CoroutineName
来涨。 - 第三個(gè)上下文在第二個(gè)的基礎(chǔ)上又增加了一個(gè)新的
CoroutineName
和Dispatchers
,同時(shí)他們也替換了第二個(gè)上下文中的CoroutineName
和Dispatchers
。
但是因?yàn)檫@個(gè)+
運(yùn)算符是不對(duì)稱的启盛,所以在我們實(shí)際的運(yùn)用過程中蹦掐,通過+
增加Element
的時(shí)候一定要注意它們結(jié)合的順序。那么現(xiàn)在關(guān)于協(xié)程上下文的內(nèi)容就講到這里僵闯,我們點(diǎn)到為止卧抗,后面在深入理解階段在細(xì)講這些東西運(yùn)行的原理細(xì)節(jié)。
協(xié)程啟動(dòng)模式
CoroutineStart
協(xié)程啟動(dòng)模式鳖粟,是啟動(dòng)協(xié)程時(shí)需要傳入的第二個(gè)參數(shù)社裆。協(xié)程啟動(dòng)有4種:
DEFAULT
默認(rèn)啟動(dòng)模式,我們可以稱之為餓漢啟動(dòng)模式向图,因?yàn)閰f(xié)程創(chuàng)建后立即開始調(diào)度泳秀,雖然是立即調(diào)度,單不是立即執(zhí)行榄攀,有可能在執(zhí)行前被取消嗜傅。LAZY
懶漢啟動(dòng)模式,啟動(dòng)后并不會(huì)有任何調(diào)度行為檩赢,直到我們需要它執(zhí)行的時(shí)候才會(huì)產(chǎn)生調(diào)度吕嘀。也就是說只有我們主動(dòng)的調(diào)用Job
的start
、join
或者await
等函數(shù)時(shí)才會(huì)開始調(diào)度漠畜。ATOMIC
一樣也是在協(xié)程創(chuàng)建后立即開始調(diào)度币他,但是它和DEFAULT
模式有一點(diǎn)不一樣,通過ATOMIC
模式啟動(dòng)的協(xié)程執(zhí)行到第一個(gè)掛起點(diǎn)之前是不響應(yīng)cancel
取消操作的憔狞,ATOMIC
一定要涉及到協(xié)程掛起后cancel
取消操作的時(shí)候才有意義蝴悉。UNDISPATCHED
協(xié)程在這種模式下會(huì)直接開始在當(dāng)前線程下執(zhí)行,直到運(yùn)行到第一個(gè)掛起點(diǎn)瘾敢。這聽起來有點(diǎn)像ATOMIC
拍冠,不同之處在于UNDISPATCHED
是不經(jīng)過任何調(diào)度器就開始執(zhí)行的尿这。當(dāng)然遇到掛起點(diǎn)之后的執(zhí)行,將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器庆杜。
我們可以通過一個(gè)小例子的來看看這幾個(gè)啟動(dòng)模式的實(shí)際情況:
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()
}
復(fù)制代碼
每個(gè)模式我們分別啟動(dòng)一個(gè)一次射众,DEFAULT
模式啟動(dòng)時(shí),我們接著調(diào)用了cancel
取消協(xié)程晃财,ATOMIC
模式啟動(dòng)時(shí),我們?cè)诶锩嬖黾恿艘粋€(gè)掛起點(diǎn)delay
掛起函數(shù)叨橱,來區(qū)分ATOMIC
啟動(dòng)時(shí)的掛起前后執(zhí)行情況,同樣的UNDISPATCHED
模式啟動(dòng)時(shí)断盛,我們也調(diào)用了cancel
取消協(xié)程罗洗,我們看實(shí)際的日志輸出情況:
D/defaultJob: CoroutineStart.DEFAULT
D/atomicJob: CoroutineStart.ATOMIC掛起前
D/undispatchedJob: CoroutineStart.UNDISPATCHED掛起前
復(fù)制代碼
或者
D/undispatchedJob: CoroutineStart.UNDISPATCHED掛起前
D/atomicJob: CoroutineStart.ATOMIC掛起前
復(fù)制代碼
為什么會(huì)出現(xiàn)2種情況。我們上面提到過DEFAULT
模式協(xié)程創(chuàng)建后立即開始調(diào)度钢猛,但不是立即執(zhí)行伙菜,所有它有可能會(huì)被cancel
取消,導(dǎo)致沒有輸出defaultJob
這條日志命迈。
同樣的ATOMIC
模式啟動(dòng)的時(shí)候也接著調(diào)用了cancel
取消協(xié)程贩绕,但是因?yàn)闆]有遇到掛起點(diǎn),所以掛起前的日志輸出了壶愤,但是掛起后的日志沒有輸出淑倾。
而UNDISPATCHED
模式啟動(dòng)的時(shí)候也接著調(diào)用了cancel
取消協(xié)程,同樣的因?yàn)闆]有遇到掛起點(diǎn)所以輸出了UNDISPATCHED掛起前
公你,但是因?yàn)?code>UNDISPATCHED是立即執(zhí)行的踊淳,所以他的日志UNDISPATCHED掛起前
輸出在ATOMIC掛起前
的前面假瞬。
接著我們?cè)谘a(bǔ)充一下關(guān)于UNDISPATCHED
模式陕靠。我們上面有提到當(dāng)以UNDISPATCHED
模式啟動(dòng)時(shí),遇到掛起點(diǎn)之后的執(zhí)行脱茉,將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器剪芥。這句話我們又要怎么理解呢。我們還是以一個(gè)例子來認(rèn)識(shí)解釋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后")
}
}
復(fù)制代碼
那我們將會(huì)看到如下輸出,掛起前后都在一個(gè)worker-1
線程里面執(zhí)行:
D/main線程: -> join前
D/DefaultDispatcher-worker-1線程: -> 掛起前
D/DefaultDispatcher-worker-1線程: -> 掛起后
D/main線程: -> join后
復(fù)制代碼
現(xiàn)在我們?cè)谏宰餍薷乃胺荆覀冊(cè)谧訁f(xié)程launch
的時(shí)候使用UNDISPATCHED
模式啟動(dòng):
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后")
}
}
復(fù)制代碼
那我們將會(huì)看到如下輸出:
D/main線程: -> 掛起前
D/main線程: -> join前
D/DefaultDispatcher-worker-1線程: -> 掛起后
D/main線程: -> join后
復(fù)制代碼
我們看到當(dāng)以UNDISPATCHED
模式即使我們指定了協(xié)程調(diào)度器Dispatchers.IO
,掛起前
還是在main
線程里執(zhí)行榜田,但是掛起后
是在worker-1
線程里面執(zhí)行益兄,這是因?yàn)楫?dāng)以UNDISPATCHED
啟動(dòng)時(shí),協(xié)程在這種模式下會(huì)直接開始在當(dāng)前線程下執(zhí)行,直到第一個(gè)掛起點(diǎn)箭券。遇到掛起點(diǎn)之后的執(zhí)行净捅,將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器,即join
處恢復(fù)執(zhí)行時(shí)辩块,因?yàn)樗诘膮f(xié)程有調(diào)度器蛔六,所以后面的執(zhí)行將會(huì)在調(diào)度器對(duì)應(yīng)的線程上執(zhí)行荆永。
我們?cè)俑囊幌拢炎訁f(xié)程在launch
的時(shí)候使用UNDISPATCHED
模式啟動(dòng)国章,去掉Dispatchers.IO
調(diào)度器具钥,那又會(huì)出現(xiàn)什么情況呢
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后")
}
}
復(fù)制代碼
D/main線程: -> 掛起前
D/main線程: -> join前
D/main線程: -> 掛起后
D/main線程: -> join后
復(fù)制代碼
我們發(fā)現(xiàn)它們都在一個(gè)線程里面執(zhí)行了。這是因?yàn)楫?dāng)通過UNDISPATCHED
啟動(dòng)后遇到掛起液兽,join
處恢復(fù)執(zhí)行時(shí)骂删,如果所在的協(xié)程沒有指定調(diào)度器,那么就會(huì)在join
處恢復(fù)執(zhí)行的線程里執(zhí)行四啰,即掛起后
是在父協(xié)程(Dispatchers.Main
線程里面執(zhí)行桃漾,而最后join后
這條日志的輸出調(diào)度取決于這個(gè)最外層的協(xié)程的調(diào)度規(guī)則。
現(xiàn)在我們可以總結(jié)一下拟逮,當(dāng)以UNDISPATCHED
啟動(dòng)時(shí):
無論我們是否指定協(xié)程調(diào)度器撬统,
掛起前
的執(zhí)行都是在當(dāng)前線程下執(zhí)行。如果所在的協(xié)程沒有指定調(diào)度器敦迄,那么就會(huì)在
join
處恢復(fù)執(zhí)行的線程里執(zhí)行恋追,即我們上述案例中的掛起后
的執(zhí)行是在main
線程中執(zhí)行。當(dāng)我們指定了協(xié)程調(diào)度器時(shí)罚屋,遇到掛起點(diǎn)之后的執(zhí)行將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器苦囱。即
join
處恢復(fù)執(zhí)行時(shí),因?yàn)樗诘膮f(xié)程有調(diào)度器脾猛,所以后面的執(zhí)行將會(huì)在調(diào)度器對(duì)應(yīng)的線程上執(zhí)行撕彤。
同樣的我們點(diǎn)到為止,關(guān)于啟動(dòng)模式的的相關(guān)內(nèi)容我們就現(xiàn)講到這里猛拴。
協(xié)程作用域
協(xié)程作用域CoroutineScope
為協(xié)程定義作用范圍羹铅,每個(gè)協(xié)程生成器launch
、async
等都是CoroutineScope
的擴(kuò)展愉昆,并繼承了它的coroutineContext
自動(dòng)傳播其所有Element
和取消职员。協(xié)程作用域本質(zhì)是一個(gè)接口,不建議手工實(shí)現(xiàn)該接口,而應(yīng)該首選委托實(shí)現(xiàn)跛溉。下面我們列出了部分CoroutineScope
相關(guān)定義:
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())
復(fù)制代碼
我們可以看到CoroutineScope
也重載了plus
方法焊切,通過+
號(hào)來新增或者修改我們CoroutineContext
協(xié)程上下文中的Element
。同時(shí)官方也為我們定義好了 MainScope
和GlobalScope
2個(gè)頂級(jí)作用域芳室。GlobalScope
我們已經(jīng)很熟了专肪,前面的案例都是通過它來實(shí)現(xiàn)的。
MainScope
我們可以看到它的上下文是通過SupervisorJob
和 Dispatchers.Main
組合的堪侯,說明它是一個(gè)在主線程執(zhí)行的協(xié)程作用域嚎尤,我們?cè)诤罄m(xù)的Android實(shí)戰(zhàn)開發(fā)中,會(huì)結(jié)合Activity抖格、Fragment诺苹,dialog等使用它咕晋。這里不再繼續(xù)往下擴(kuò)展。
至于SupervisorJob
分析它之前收奔,我們得先說一下協(xié)程作用域的分類掌呜。我們之前提到過父協(xié)程和子協(xié)程的概念,既然有父協(xié)程和子協(xié)程坪哄,那么必然也有父協(xié)程作用域和子父協(xié)程作用域质蕉。不過我們不是這么稱呼,因?yàn)樗麄儾粌H僅是父與子的概念翩肌。協(xié)程作用域分為三種:
頂級(jí)作用域
--> 沒有父協(xié)程的協(xié)程所在的作用域稱之為頂級(jí)作用域模暗。協(xié)同作用域
--> 在協(xié)程中啟動(dòng)一個(gè)協(xié)程,新協(xié)程為所在協(xié)程的子協(xié)程念祭。子協(xié)程所在的作用域默認(rèn)為協(xié)同作用域。此時(shí)子協(xié)程拋出未捕獲的異常時(shí)粱坤,會(huì)將異常傳遞給父協(xié)程處理隶糕,如果父協(xié)程被取消,則所有子協(xié)程同時(shí)也會(huì)被取消站玄。主從作用域
官方稱之為監(jiān)督作用域
枚驻。與協(xié)同作用域一致,區(qū)別在于該作用域下的協(xié)程取消操作的單向傳播性株旷,子協(xié)程的異常不會(huì)導(dǎo)致其它子協(xié)程取消再登。但是如果父協(xié)程被取消,則所有子協(xié)程同時(shí)也會(huì)被取消晾剖。
同時(shí)補(bǔ)充一點(diǎn):父協(xié)程需要等待所有的子協(xié)程執(zhí)行完畢之后才會(huì)進(jìn)入Completed
狀態(tài)锉矢,不管父協(xié)程自身的協(xié)程體是否已經(jīng)執(zhí)行完成。我們?cè)谧铋_始提到協(xié)程生命周期的時(shí)候就提到過下钞瀑,現(xiàn)在回過頭看是不是感覺很流程變得清晰沈撞。
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
復(fù)制代碼
子協(xié)程會(huì)繼承父協(xié)程的協(xié)程上下文中的Element
慷荔,如果自身有相同key的成員雕什,則覆蓋對(duì)應(yīng)的key
,覆蓋的效果僅限自身范圍內(nèi)有效显晶。這個(gè)就可以用上我們前面學(xué)到的協(xié)程上下文CoroutineContext
的知識(shí)贷岸,小案例奉上:
private fun testCoroutineScope(){
GlobalScope.launch(Dispatchers.Main){
Log.d("父協(xié)程上下文", "$coroutineContext")
launch(CoroutineName("第一個(gè)子協(xié)程")) {
Log.d("第一個(gè)子協(xié)程上下文", "$coroutineContext")
}
launch(Dispatchers.Unconfined) {
Log.d("第二個(gè)子協(xié)程協(xié)程上下文", "$coroutineContext")
}
}
}
復(fù)制代碼
日志順序的問題我們前面已經(jīng)分析過原因,如果還不懂的話磷雇,麻煩您回到基礎(chǔ)用法里面仔細(xì)的再看一遍偿警。
D/父協(xié)程上下文: [StandaloneCoroutine{Active}@81b6e46, Dispatchers.Main]
D/第二個(gè)子協(xié)程協(xié)程上下文: [StandaloneCoroutine{Active}@f6b7807, Dispatchers.Unconfined]
D/第一個(gè)子協(xié)程上下文: [CoroutineName(第一個(gè)子協(xié)程), StandaloneCoroutine{Active}@bbe6d34, Dispatchers.Main]
復(fù)制代碼
可以看到第一個(gè)子協(xié)程的覆蓋了父協(xié)程的Job
,但是它繼承了父協(xié)程的調(diào)度器 Dispatchers.Main
,同時(shí)也新增了一個(gè)CoroutineName
唯笙。第二個(gè)子協(xié)程覆蓋了父協(xié)程的Job
螟蒸,也將父協(xié)程的調(diào)度器覆蓋為Unconfined
盒使,但是他沒有繼承第一個(gè)子協(xié)程的CoroutineName
,這就是我們說的覆蓋的效果僅限自身范圍內(nèi)有效七嫌。接下來我們看看上面提到的協(xié)同作用域
和主從(監(jiān)督)作用域
異常傳遞和協(xié)程取消的問題少办。
我們上面提到協(xié)同作用域
如果子協(xié)程拋出未捕獲的異常時(shí),會(huì)將異常傳遞給父協(xié)程處理诵原,如果父協(xié)程被取消英妓,則所有子協(xié)程同時(shí)也會(huì)被取消。先上代碼看看效果:
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")
}
}
復(fù)制代碼
D/scope: --------- 1
D/scope: --------- 2
D/exceptionHandler: CoroutineName(scope1) java.lang.NullPointerException: 空指針
復(fù)制代碼
可以看到子協(xié)程scope2
拋出了一個(gè)異常绍赛,將異常傳遞給父協(xié)程scope1
處理蔓纠,但是因?yàn)槿魏我粋€(gè)子協(xié)程異常退出會(huì)導(dǎo)致整體都將退出。所以導(dǎo)致父協(xié)程scope1
未執(zhí)行完成成就被取消吗蚌,同時(shí)還未執(zhí)行完子協(xié)程scope3
也被取消了腿倚。
主從(監(jiān)督)作用域
與協(xié)同作用域
一致,區(qū)別在于該作用域下的協(xié)程取消操作的單向傳播性蚯妇,子協(xié)程的異常不會(huì)導(dǎo)致其它子協(xié)程取消猴誊。分析主從(監(jiān)督)作用域
的時(shí)候,我們需要用到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")
}
}
}
復(fù)制代碼
D/scope: --------- 1
D/scope: --------- 2
D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指針
D/scope: --------- 6
D/scope: --------- 7
D/scope: --------- 8
復(fù)制代碼
可以看到子協(xié)程scope2
拋出了一個(gè)異常懈叹,并將異常傳遞給父協(xié)程scope1
處理,同時(shí)也結(jié)束了自己本身分扎。因?yàn)樵谟?code>主從(監(jiān)督)作用域下的協(xié)程取消操作是單向傳播性澄成,因此協(xié)程scope2
的異常并沒有導(dǎo)致父協(xié)程退出,所以6
7
8
都照常輸出畏吓,而3
4
5
因?yàn)樵趨f(xié)程scope2
里面所以沒有輸出墨状。
我們剛剛使用了supervisorScope
實(shí)現(xiàn)了主從(監(jiān)督)作用域
,那我們通過SupervisorJob
又該如何實(shí)現(xiàn)呢。我們把supervisorScope
稱之為主從(監(jiān)督)作用域
菲饼,那么SupervisorJob
就可以稱之為主從(監(jiān)督)作業(yè)
肾砂,如下:
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]}")
}
}
復(fù)制代碼
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)
復(fù)制代碼
是不是感覺和supervisorScope
的用法很像,我們通過創(chuàng)建了一個(gè)SupervisorJob
的主從(監(jiān)督)協(xié)程作用域,調(diào)用了子協(xié)程的join
是為了保證它一定是會(huì)執(zhí)行宏悦。同樣的子協(xié)程scope2
拋出了一個(gè)異常镐确,通過協(xié)程scope2
自己內(nèi)部消化了,同時(shí)也結(jié)束了自己本身饼煞。
因?yàn)閰f(xié)程scope2
的異常并沒有導(dǎo)致coroutineScope
作用域下的協(xié)程取消退出源葫,所以協(xié)程scope3
照常運(yùn)行輸出2
,后又因?yàn)檎{(diào)用了我們定義的協(xié)程作用域coroutineScope
的cancel
方法取消了協(xié)程砖瞧,所以即使我們后面調(diào)用了協(xié)程scope3
的join
,也沒有輸出3
,因?yàn)?code>SupervisorJob的取消是向下傳播的息堂,所以后面的4
5
都是在coroutineScope
的作用域中輸出的。
現(xiàn)在我們關(guān)于協(xié)程作用域CoroutineScope
的作用我們已經(jīng)有了一個(gè)大概的了解,同樣的因?yàn)檫@個(gè)篇幅中我們是基礎(chǔ)講解荣堰,所以我們點(diǎn)到為止床未,如果還想深入了解,那就只能看后面的深入?yún)f(xié)程篇幅振坚。
掛起函數(shù)
通過前面的篇幅我們已經(jīng)知道,使用suspend
關(guān)鍵字修飾的函數(shù)叫作掛起函數(shù)
即硼,掛起函數(shù)
只能在協(xié)程體內(nèi),或著在其他掛起函數(shù)
內(nèi)調(diào)用屡拨。那掛起又是啥玩意呢只酥。
我估計(jì)各位看到這里的時(shí)候,可能有些人已經(jīng)被上面的知識(shí)點(diǎn)弄的有點(diǎn)暈乎呀狼,別急裂允,先放松下大腦,喝杯水哥艇,然后做個(gè)眼保健操緩解一下绝编。下面開始敲黑板了,打起精神貌踏,要開始劃重點(diǎn)了十饥。
首先一個(gè)掛起函數(shù)
既然要掛起,那么他必定得有一個(gè)掛起點(diǎn)
祖乳,不然我們?cè)趺粗篮瘮?shù)是否掛起逗堵,從哪掛起呢。 我們定義一個(gè)空實(shí)現(xiàn)的suspend
方法眷昆,然后通過AS的工具欄中Tools
->kotlin
->show kotlin ByteCode
解析成字節(jié)碼
private suspend fun test(){
}
復(fù)制代碼
final synthetic test(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
復(fù)制代碼
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
復(fù)制代碼
我們看到test
方法需要的是一個(gè)Continuation
接口蜒秤,官方給的介紹是用于掛起點(diǎn)之后,返回類型為T
的值用的亚斋。那我們又是怎么拿到的這個(gè)Continuation
呢作媚。要解開這個(gè)問題我們得先回到協(xié)程的創(chuàng)建和運(yùn)行是的過程。
我們啟動(dòng)一個(gè)協(xié)程無非是通過launch
帅刊,async
等方法纸泡。我們之前有說到過他們的啟動(dòng)模式CoroutineStart
,但是并沒有深入的去分析它的創(chuàng)建和啟動(dòng)過程赖瞒,我們這里先回過頭大概的看一下:
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
}
復(fù)制代碼
我們看到在通過launch
啟動(dòng)一個(gè)協(xié)程的時(shí)候女揭,他通過coroutine
的start
方法啟動(dòng)協(xié)程,然后我們接著往下看
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)
}
復(fù)制代碼
然后start
方法里面調(diào)用了CoroutineStart
的invoke
冒黑,這個(gè)時(shí)候我們發(fā)現(xiàn)了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
}
復(fù)制代碼
而Continuation
又是通過start
方法傳進(jìn)來的coroutine
。所以現(xiàn)在可以確定抡爹,我們的協(xié)程體本身就是一個(gè)Continuation
,這也就解釋了為什么可以在協(xié)程體內(nèi)調(diào)用suspend
掛起函數(shù)了芒划。
現(xiàn)在我們也可以確定冬竟,在協(xié)程內(nèi)部掛起函數(shù)
的調(diào)用處就是掛起點(diǎn)
欧穴,如果掛起點(diǎn)
出現(xiàn)異步調(diào)用,那么當(dāng)前協(xié)程就被掛起泵殴,直到對(duì)應(yīng)的Continuation
通過調(diào)用resumeWith
函數(shù)才會(huì)恢復(fù)協(xié)程的執(zhí)行涮帘,同時(shí)返回Result<T>
類型的成功或者失敗的結(jié)果。
由于章節(jié)主題的限制笑诅,這里我們就不再下深入了调缨。需要注意的是掛起函數(shù)
不一定真的會(huì)掛起,如果只是提供了掛起的條件吆你,但是協(xié)程沒有產(chǎn)生異步調(diào)用弦叶,那么協(xié)程還是不會(huì)被掛起。
預(yù)告:下一篇我們將會(huì)講解kotlin協(xié)程中的異常處理妇多,其實(shí)我們?cè)谶@篇章節(jié)中已經(jīng)伤哺,提到了一些異常處理,沒有注意的同學(xué)可以回到協(xié)程作用域
看看者祖。
作者:一個(gè)被攝影耽誤的程序猿
來源:https://juejin.cn/post/6953287252373930021
著作權(quán)歸作者所有立莉。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處七问。