一. kotlin協(xié)程的理解
協(xié)程并不是一個(gè)新的概念,而是一個(gè)非常老的概念,很多語(yǔ)言都支持協(xié)程,建議去瀏覽器去了解一下協(xié)程的歷史和基本概念,這里我們只講一下kotlin中的協(xié)程的作用。
從代碼實(shí)現(xiàn)角度來看:kotlin協(xié)程底層是用線程實(shí)現(xiàn)的,是一個(gè)封裝完善供開發(fā)者使用的線程框架。kotlin的一個(gè)協(xié)程可以理解為是運(yùn)行在線程上的一個(gè)執(zhí)行任務(wù)并且該任務(wù)可以在不同的線程間切換,一個(gè)線程可以同時(shí)運(yùn)行多個(gè)協(xié)程
概念圖如下:一個(gè)線程可以同時(shí)運(yùn)行多個(gè)協(xié)程(執(zhí)行任務(wù)),如果有需要,線程1上的協(xié)程可以切換到線程2上去運(yùn)行。反應(yīng)在我們Android開發(fā)中,就是我們的網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)庫(kù)操作等耗時(shí)任務(wù)時(shí)運(yùn)行在IO線程的,當(dāng)我們獲取到數(shù)據(jù)以后更新頁(yè)面的操作是運(yùn)行在主線程的
從開發(fā)者角度來看:kotlin協(xié)程可以實(shí)現(xiàn)以同步的方式去編寫異步執(zhí)行的代碼,解決線程切換回調(diào)的嵌套地獄。協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無代價(jià)的。
反應(yīng)到我們Android開發(fā)就是,以前我們?cè)贗O線程拿到數(shù)據(jù)后,想回到主線程更新UI,我們一般會(huì)使用Handler或者CallBack接口回調(diào)等方式,這樣做的結(jié)果是當(dāng)我們連續(xù)進(jìn)行上述操作后很容易陷入嵌套地獄,嚴(yán)重影響代碼閱讀性和美觀。
二. 協(xié)程的創(chuàng)建方式
我們想在Android Studio中使用協(xié)程,除了引入kotlin支持外,我們還要引入兩個(gè)協(xié)程支持庫(kù)
// 協(xié)程核心庫(kù)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1"
// 協(xié)程Android支持庫(kù)
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1"
創(chuàng)建協(xié)程的方式
2.1.) runBlocking:這是一個(gè)頂層函數(shù),會(huì)啟動(dòng)一個(gè)新的協(xié)程并阻塞調(diào)用它的線程,直到里面的代碼執(zhí)行完畢,返回值是泛型T。
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
2.2) CoroutineScope.launch :通過一個(gè)協(xié)程作用域的擴(kuò)展方法launch啟動(dòng)一個(gè)協(xié)程,不會(huì)阻塞調(diào)用它的線程,返回值是Job。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
2.3) CoroutineScope.async :通過一個(gè)協(xié)程作用域的擴(kuò)展方法async啟動(dòng)一個(gè)協(xié)程,不會(huì)阻塞調(diào)用它的線程,返回值是 Deferred。
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
我們一般使用2)3)兩種方式開啟一個(gè)協(xié)程,1)方式因?yàn)闀?huì)阻塞線程,所以runBlocking函數(shù)我們?cè)陂_發(fā)中基本不會(huì)使用到,但可以用于代碼調(diào)試。
提前說一下async和launch的區(qū)別,后面會(huì)講到并有示例:
返回值不同:async函數(shù)體中最后一行代碼表達(dá)式運(yùn)行結(jié)果會(huì)作為結(jié)果返回,也就是Deferred中的泛型T,我們可以通過其他協(xié)程函數(shù)獲取到這個(gè)執(zhí)行結(jié)果,而launch沒有這樣的返回值。
示例代碼:GlobalScope是一個(gè)協(xié)程庫(kù)中已經(jīng)提供的協(xié)程作用域
runBlocking {
Log.e("協(xié)程","我們使用runBlocking啟動(dòng)了一個(gè)協(xié)程")
}
GlobalScope.launch {
Log.e("協(xié)程","我們使用launch啟動(dòng)了一個(gè)協(xié)程")
}
GlobalScope.async {
Log.e("協(xié)程","我們使用async啟動(dòng)了一個(gè)協(xié)程")
}
三. 協(xié)程作用域與協(xié)程上下文
我們前面提到創(chuàng)建協(xié)程的三種方式中有l(wèi)aunch和async方式都需要使用協(xié)程作用域開啟一個(gè)協(xié)程
3.1 什么是協(xié)程作用域?
協(xié)程作用域CoroutineScope是協(xié)程的運(yùn)行范圍。
如下:CoroutineScope是一個(gè)接口,只有一個(gè)接口屬性CoroutineContext 協(xié)程上下文,所以CoroutineScope其實(shí)是CoroutineContext協(xié)程上下文的封裝。
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
3.2 什么是協(xié)程上下文?
CoroutineContext代表協(xié)程的上下文,我們?cè)诶斫鈪f(xié)程上下文之前,我們先理解一下Context上下文,Android開發(fā)者應(yīng)該對(duì)這個(gè)再屬性不過了,我們的Application、Activity等都被定義成了一個(gè)Context。那我們?cè)趺蠢斫膺@個(gè)Context上下文呢?好比我們上學(xué)考試的時(shí)候進(jìn)行閱讀理解,我們要理解一段話的含義,我們應(yīng)該怎么辦,需要代入原文章,然后看看前面看看后面是怎么回事我們才能懂。代入代碼中,我們運(yùn)行到這段代碼了,我們也需要了解一下這段代碼該怎么執(zhí)行,需要什么信息才能執(zhí)行,這些信息就保存在Context中,我們可以把Context理解為一個(gè)容器,用于保存代碼執(zhí)行所需要的配置信息。所以CoroutineContext就是我們啟動(dòng)一個(gè)協(xié)程需要的配置信息。
我們看一下CoroutineContext的源碼實(shí)現(xiàn),是一個(gè)接口,該接口內(nèi)部有一個(gè)內(nèi)部接口Element(該接口繼承自CoroutineContext,Element接口有一個(gè)key,類似于鍵值對(duì),我們可以用key判斷Element)
我們看到我們可以對(duì)CoroutineContext執(zhí)行 get(根據(jù)key獲取某個(gè)CoroutineContext)、plus(一個(gè)加號(hào)操作符重載返回值是CoroutineContext,這就意味著CoroutineContext+CoroutineContext =CoroutineContext )、minusKey(去掉key對(duì)應(yīng)的CoroutineContext)等操作,看著真像是一個(gè)集合操作,其實(shí)CoroutineContext就類似于一個(gè)集合操作,所以我們可以猜想一下CoroutineContext有很多的子類,這些子類可以相加拼接,minusKey相減構(gòu)成多樣性的CoroutineContext。
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E?
@Suppress("UNCHECKED_CAST")
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
}
}
3.3 協(xié)程作用域與子協(xié)程
我們可以在一個(gè)協(xié)程中開啟另一個(gè)協(xié)程,這樣開啟的協(xié)程就是子協(xié)程。
需要注意的有兩點(diǎn):
1.)子協(xié)程的協(xié)程作用域會(huì)繼承父協(xié)程協(xié)程作用域里的 協(xié)程上下文
2.)如果父協(xié)程取消了,所有的子協(xié)程也會(huì)被取消
我們會(huì)在下面依次驗(yàn)證這兩個(gè)注意點(diǎn)。
3.4 CoroutineContext的那些子類
CoroutineContext有很多子類,這些子類各自有不同的的作用,并且共同構(gòu)成了協(xié)程作用域里的CoroutineContext。
我們先打印一下一個(gè)協(xié)程作用域的的coroutineContext,看打印出來是什么
GlobalScope.launch{
Log.e("協(xié)程的coroutineContext",this.coroutineContext.toString())
}
打印結(jié)果:
[StandaloneCoroutine{Active}@5a7652d, Dispatchers.Default]
我們一一了解這些子類,并且了解他們對(duì)協(xié)程的影響和作用
3.4.1 )CoroutineDispatcher 協(xié)程調(diào)度器
我們知道協(xié)程運(yùn)行在線程,并且可以切換到其他線程,CoroutineDispatcher確定了相關(guān)的協(xié)程在哪個(gè)線程或哪些線程上執(zhí)行,所以也可以把CoroutineDispatcher叫做協(xié)程的線程調(diào)度器。
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
@ExperimentalStdlibApi
public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
ContinuationInterceptor,
{ it as? CoroutineDispatcher })
}
kotlin給我們提供了四種調(diào)度器
public actual object Dispatchers {
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
Default:默認(rèn)調(diào)度器,CPU密集型任務(wù)調(diào)度器,通常處理一些單純的計(jì)算任務(wù),或者執(zhí)行時(shí)間較短任務(wù)。例如數(shù)據(jù)計(jì)算
IO:IO調(diào)度器,IO密集型任務(wù)調(diào)度器,適合執(zhí)行IO相關(guān)操作。比如:網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)庫(kù)操作,文件操作等
Main:UI調(diào)度器,只有在UI編程平臺(tái)上有意義,用于更新UI,例如Android中的主線程
Unconfined:非受限調(diào)度器,無所謂調(diào)度器,當(dāng)前協(xié)程可以運(yùn)行在任意線程上
我們通過上面的打印可以知道,GlobalScope的協(xié)程調(diào)度器是Dispatchers.Default,那么我們?nèi)绾胃淖兡兀课覀兦懊娌榭磍aunch和async方法時(shí),看到他們的第一個(gè)參數(shù)都是context: CoroutineContext ,是的,我們可以從這里傳入我們需要的上下文,并且會(huì)覆蓋掉協(xié)程作用域里的上下文。
如下:我們希望開啟一個(gè)協(xié)程運(yùn)行在IO線程上
GlobalScope.launch(Dispatchers.IO){
Log.e("協(xié)程的coroutineContext",this.coroutineContext.toString())
}
打印結(jié)果:
[StandaloneCoroutine{Active}@db90566, Dispatchers.IO]
那如果我們想在協(xié)程運(yùn)行中改變線程怎么辦,最常見的,網(wǎng)絡(luò)請(qǐng)求在IO線程,而頁(yè)面更新在主線程。kotlin給我們提供了一個(gè)頂層函數(shù)withContext用于改變協(xié)程的上下文并執(zhí)行一段代碼。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
示例代碼:
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) {
//網(wǎng)絡(luò)請(qǐng)求
"返回結(jié)果"
}
mBtn.text = result
}
3.4.2 )CoroutineName 協(xié)程名稱
協(xié)程名稱:顧名思義,就是給協(xié)程取個(gè)名字,這個(gè)沒有什么好說的,就是用來更好的區(qū)分協(xié)程的。
public data class CoroutineName(
val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
public companion object Key : CoroutineContext.Key<CoroutineName>
override fun toString(): String = "CoroutineName($name)"
}
示例代碼:
GlobalScope.launch(Dispatchers.Main + CoroutineName("主協(xié)程")) {
Log.e("協(xié)程的coroutineContext",this.coroutineContext.toString())
}
打印結(jié)果:
[CoroutineName(主協(xié)程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main]
我們使用協(xié)程名稱驗(yàn)證子協(xié)程的第一個(gè)注意點(diǎn):子協(xié)程的協(xié)程作用域會(huì)繼承父協(xié)程協(xié)程作用域里的 協(xié)程上下文
我們?cè)谏鲜龃a中開啟一個(gè)子協(xié)程:
GlobalScope.launch(Dispatchers.Main + CoroutineName("主協(xié)程")) {
Log.e("協(xié)程的coroutineContext" , this.coroutineContext.toString())
launch {
Log.e("協(xié)程的coroutineContext2" , this.coroutineContext.toString())
}
}
打印結(jié)果如下:可以看到,子協(xié)程竟然打印出了和父協(xié)程相同的CoroutineName,說明父協(xié)程的協(xié)程上下文傳播到了子協(xié)程
協(xié)程的coroutineContext: [CoroutineName(主協(xié)程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main]
協(xié)程的coroutineContext2: [CoroutineName(主協(xié)程), StandaloneCoroutine{Active}@b95b3e, Dispatchers.Main]
我們稍微改一下代碼:
GlobalScope.launch(Dispatchers.Main + CoroutineName("主協(xié)程")) {
Log.e("協(xié)程的coroutineContext" , this.coroutineContext.toString())
launch(CoroutineName("子協(xié)程")) {
Log.e("協(xié)程的coroutineContext2" , this.coroutineContext.toString())
}
}
打印結(jié)果如下:子協(xié)程設(shè)置的協(xié)程上下文覆蓋掉了從父協(xié)程處繼承的上下文
協(xié)程的coroutineContext: [CoroutineName(主協(xié)程), StandaloneCoroutine{Active}@288ff9, Dispatchers.Main]
協(xié)程的coroutineContext2: [CoroutineName(子協(xié)程), StandaloneCoroutine{Active}@8aced9f, Dispatchers.Main]
3.4.3)Job與協(xié)程的生命周期
我們前面在查看launch和async兩個(gè)擴(kuò)展函數(shù)時(shí),可以看到launch返回結(jié)果是一個(gè)Job,而async的返回結(jié)果是一個(gè)Deferred,Deferred其實(shí)是Job的子類。
public interface Job : CoroutineContext.Element
public interface Deferred<out T> : Job
那么Job是什么呢?
協(xié)程啟動(dòng)以后,我們可以得到一個(gè)Job對(duì)象,通過Job對(duì)象我們可以檢測(cè)協(xié)程的生命周期狀態(tài),并且可以操作協(xié)程(比如取消協(xié)程)。我們可以大致把Job理解為協(xié)程本身。
協(xié)程的生命周期:協(xié)程創(chuàng)建以后,處于New(新建)狀態(tài),協(xié)程啟動(dòng)(調(diào)用start()方法)以后,處于Active(活躍) 狀態(tài),協(xié)程及所有子協(xié)程完成任務(wù)以后,處于Completed(完成) 狀態(tài),協(xié)程被取消(調(diào)用cancel()方法)以后,處于Cancelled(取消) 狀態(tài)
我們可以使用job下面的字段檢查協(xié)程的狀態(tài):
isActive 用于判斷協(xié)程是否處于活躍狀態(tài),
isCancelled 用于判斷協(xié)程是否被取消
isCompleted用于判斷協(xié)程是否結(jié)束
除了獲取協(xié)程狀態(tài),還有很多可以用于操縱協(xié)程的函數(shù),例如:
cancel()取消協(xié)程。
start()啟動(dòng)協(xié)程。
await() 等待協(xié)程執(zhí)行完成
我們驗(yàn)證一下協(xié)程的生命周期:
GlobalScope.launch {
val job = launch(CoroutineName("子協(xié)程")) {
}
Log.e("子協(xié)程的狀態(tài)","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
delay(1000)
Log.e("子協(xié)程的狀態(tài)2","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
}
打印結(jié)果:
子協(xié)程的狀態(tài): true false false
子協(xié)程的狀態(tài)2: false false true
GlobalScope.launch {
val job = launch(CoroutineName("子協(xié)程")) {
delay(5000)
}
Log.e("子協(xié)程的狀態(tài)","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
job.cancel()
Log.e("取消后子協(xié)程的狀態(tài)","${job.isActive} ${job.isCancelled} ${job.isCompleted}")
}
打印結(jié)果:
子協(xié)程的狀態(tài): true false false
取消后子協(xié)程的狀態(tài): false true false
我們使用協(xié)程的生命周期驗(yàn)證一下子協(xié)程的第二個(gè)注意點(diǎn):如果父協(xié)程取消了,所有的子協(xié)程也會(huì)被取消
var childJob : Job? = null
val parentJob = GlobalScope.launch {
childJob = launch(CoroutineName("子協(xié)程")) {
delay(5000)
}
}
Log.e("父協(xié)程的狀態(tài)" , "${parentJob.isActive} ${parentJob.isCancelled} ${parentJob.isCompleted}")
Handler().postDelayed(Runnable {
Log.e("子協(xié)程的狀態(tài)" ,
"${childJob?.isActive} ${childJob?.isCancelled} ${childJob?.isCompleted}")
parentJob.cancel()
Log.e("父協(xié)程的狀態(tài)" ,
"${parentJob.isActive} ${parentJob.isCancelled} ${parentJob.isCompleted}")
Log.e("子協(xié)程的狀態(tài)" ,
"${childJob?.isActive} ${childJob?.isCancelled} ${childJob?.isCompleted}")
} , 1000)
打印結(jié)果如下:可以看到父協(xié)程取消以后,子協(xié)程也取消了。
父協(xié)程的狀態(tài): true false false
子協(xié)程的狀態(tài): true false false
父協(xié)程的狀態(tài): false true false
子協(xié)程的狀態(tài): false true false
3.4.4) CoroutineExceptionHandler 協(xié)程異常處理
我們?cè)趯懘a的時(shí)候,肯定會(huì)遇到異常的情況,正常我們處理異常使用try …catch捕獲,但難免會(huì)出現(xiàn)遺漏。CoroutineExceptionHandler 就是協(xié)程專門捕獲異常的類,協(xié)程中出現(xiàn)的異常都會(huì)被捕獲并由CoroutineExceptionHandler的handleException方法返回給我們進(jìn)行處理。
public interface CoroutineExceptionHandler : CoroutineContext.Element {
public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
public fun handleException(context: CoroutineContext, exception: Throwable)
}
handleException會(huì)返回兩個(gè)參數(shù),第一個(gè)參數(shù)是出現(xiàn)異常的協(xié)程,第二個(gè)參數(shù)是出現(xiàn)的異常。
示例如下:我們手動(dòng)拋出一個(gè)NullPointerException異常,然后創(chuàng)建一個(gè)CoroutineExceptionHandler賦給協(xié)程。
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.e("捕獲異常", "${coroutineContext[CoroutineName]} :$throwable")
}
GlobalScope.launch(Dispatchers.Main + CoroutineName("主協(xié)程")+exceptionHandler) {
Log.e("協(xié)程的coroutineContext",this.coroutineContext.toString())
throw NullPointerException()
}
打印結(jié)果:
協(xié)程的coroutineContext: [CoroutineName(主協(xié)程), com.jf.simple.ThirdActivity$onCreate$$inlined$CoroutineExceptionHandler$1@288ff9, StandaloneCoroutine{Active}@b95b3e, Dispatchers.Main]
捕獲異常: CoroutineName(主協(xié)程) :java.lang.NullPointerException
3.4.5) ContinuationInterceptor 協(xié)程攔截器
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
顧名思義,是用來攔截協(xié)程的,因?yàn)樯婕暗綊炱鸷瘮?shù)的原理,并且平時(shí)開發(fā)用到的相對(duì)較少,所以這里不展開。
四、協(xié)程的啟動(dòng)模式
我們?cè)诓榭磍aunch和async擴(kuò)展函數(shù)時(shí),還有第二個(gè)參數(shù),start: CoroutineStart,這個(gè)參數(shù)的含義就是協(xié)程的啟動(dòng)模式,
public enum class CoroutineStart {
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED;
}
可以看到CoroutineStart是一個(gè)枚舉類,有四種類型。
DEFAULT默認(rèn)啟動(dòng)模式,協(xié)程創(chuàng)建后立即開始調(diào)度,注意是立即調(diào)度而不是立即執(zhí)行,可能在執(zhí)行前被取消掉。
LAZY懶漢啟動(dòng)模式,創(chuà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模式是有區(qū)別的,該模式下協(xié)程啟動(dòng)以后需要執(zhí)行到第一個(gè)掛起點(diǎn)才會(huì)響應(yīng)cancel操作。
UNDISPATCHED協(xié)程在這種模式下會(huì)直接開始在當(dāng)前線程下執(zhí)行,直到運(yùn)行到第一個(gè)掛起點(diǎn)。和ATOMIC很像,但UNDISPATCHED很受調(diào)度器的影響
示例代碼:
DEFAULT:代碼立即打印,說明協(xié)程創(chuàng)建后立即調(diào)度
GlobalScope.launch {
Log.e("default啟動(dòng)模式", "協(xié)程運(yùn)行")
}
LAZY:未調(diào)用start()方法前,無打印,調(diào)用start()方法后,代碼打印。協(xié)程說明創(chuàng)建后不會(huì)調(diào)度,需要我們手動(dòng)啟動(dòng)。
val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
Log.e("lazy啟動(dòng)模式", "協(xié)程運(yùn)行")
}
//lazyJob.start()
ATOMIC:協(xié)程運(yùn)行后運(yùn)行到第一個(gè)掛起函數(shù)后才會(huì)響應(yīng)cancel()方法。
val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
Log.e("atomic啟動(dòng)模式" , "運(yùn)行到掛起函數(shù)前")
delay(100)
Log.e("atomic啟動(dòng)模式" , "運(yùn)行到掛起函數(shù)后")
}
atomicJob.cancel()
打印結(jié)果:
atomic啟動(dòng)模式: 運(yùn)行到掛起函數(shù)前
UNDISPATCHED:可以看到結(jié)果和ATOMIC很像,但因?yàn)锳TOMIC和UNDISPATCHED開發(fā)中用的相對(duì)較少,這里不過多區(qū)分,有興趣的可以開啟一個(gè)子協(xié)程添加調(diào)度器執(zhí)行一下代碼,看看區(qū)別。
五、掛起函數(shù)
我們前面提到:kotlin協(xié)程最大的優(yōu)勢(shì)就是以同步的方式寫異步代碼,這就是通過掛起函數(shù)用來實(shí)現(xiàn)
被關(guān)鍵字suspend修飾的函數(shù)稱為掛起函數(shù),掛起函數(shù)只能在協(xié)程或者另一個(gè)掛起函數(shù)中調(diào)用。掛起函數(shù)的特點(diǎn)是“掛起與恢復(fù)”,當(dāng)協(xié)程遇到掛起函數(shù)時(shí),協(xié)程會(huì)被掛起,等掛起函數(shù)執(zhí)行完畢以后,協(xié)程會(huì)恢復(fù)到掛起的地方重新運(yùn)行。
掛起是非阻塞性的掛起,不會(huì)阻塞線程;恢復(fù)不用我們手動(dòng)恢復(fù),而是協(xié)程幫我們完成。
我們?cè)谶@里演示兩個(gè)例子,來理解掛起函數(shù)的使用。(這里不講掛起函數(shù)的原理,后面會(huì)專門開篇。)
5.1 )順序執(zhí)行異步代碼
示例代碼1:我們定義兩個(gè)掛起函數(shù),一個(gè)延遲1s,一個(gè)延遲2s(來模擬網(wǎng)絡(luò)請(qǐng)求),最后都返回一個(gè)整數(shù),我們想把結(jié)果相加。不使用協(xié)程的情況下,因?yàn)檠舆t不一樣,我們需要使用類似回調(diào)的方式去獲取結(jié)果。但使用協(xié)程就不一樣,我們從打印結(jié)果看到,代碼完全是順序執(zhí)行的,measureTimeMillis方法可以測(cè)量運(yùn)行時(shí)間,可以看到運(yùn)行時(shí)間是3s多一點(diǎn):所以整個(gè)過程是這樣的,協(xié)程運(yùn)行到returnNumber1(),檢測(cè)到是一個(gè)掛起函數(shù),協(xié)程掛起,等待returnNumber1()完成,returnNumber1()運(yùn)行完成花了1s,協(xié)程回到returnNumber1()被調(diào)用的地方,獲取結(jié)果,繼續(xù)往下執(zhí)行,行到returnNumber2(),檢測(cè)到是一個(gè)掛起函數(shù),協(xié)程掛起,等待returnNumber2()完成,returnNumber2()運(yùn)行完成花了2s,協(xié)程回到returnNumber2()被調(diào)用的地方,繼續(xù)執(zhí)行。
suspend fun returnNumber1() : Int {
delay(1000L)
Log.e("returnNumber1" , "調(diào)用了returnNumber1()方法")
return 1
}
suspend fun returnNumber2() : Int {
delay(2000L)
Log.e("returnNumber1" , "調(diào)用了returnNumber2()方法")
return 2
}
GlobalScope.launch {
val time = measureTimeMillis {
val number1 = returnNumber1()
Log.e("number1" , "需要獲取number1")
val number2 = returnNumber2()
Log.e("number2" , "需要獲取number2")
val result = number1 + number2
Log.e("執(zhí)行完畢" , result.toString())
}
Log.e("運(yùn)行時(shí)間",time.toString())
}
打印結(jié)果:
returnNumber1: 調(diào)用了returnNumber1()方法
number1: 需要獲取number1
returnNumber1: 調(diào)用了returnNumber2()方法
number2: 需要獲取number2
執(zhí)行完畢: 3
運(yùn)行時(shí)間: 3010
5.2) async實(shí)現(xiàn)并發(fā)
注意,掛起函數(shù)掛起的是當(dāng)前的協(xié)程,不會(huì)影響其他的協(xié)程
我們修改一下上面的代碼,將兩個(gè)掛起函數(shù)放在兩個(gè)子協(xié)程中,最終結(jié)果使用await()獲取,await()方法的作用是等待協(xié)程運(yùn)行結(jié)束并獲取返回結(jié)果,我們前面說過launch和async的區(qū)別,一個(gè)執(zhí)行結(jié)果的返回值,一個(gè)沒有,所以這里使用async。
示例代碼:這里使用async開啟了兩個(gè)子協(xié)程,兩個(gè)子協(xié)程都有掛起函數(shù),所以兩個(gè)子協(xié)程都會(huì)被掛起,但他們的父協(xié)程在調(diào)用await()掛起函數(shù)之前沒有都沒有被掛起,所以可以正常運(yùn)行,兩個(gè)子協(xié)程并發(fā)執(zhí)行,最終通過打印的執(zhí)行時(shí)間也可以看出來,代碼執(zhí)行也就2s文章來源:http://www.zghlxwxcb.cn/news/detail-704495.html
GlobalScope.launch(Dispatchers.Main) {
val time = measureTimeMillis {
val deferred1 = async {
Log.e("--" , "子協(xié)程1運(yùn)行開始")
returnNumber1()
}
Log.e("--" , "開始運(yùn)行第二個(gè)協(xié)程")
val deferred2 = async {
Log.e("--" , "子協(xié)程2運(yùn)行開始")
returnNumber2()
}
Log.e("--" , "開始計(jì)算結(jié)果")
val result = deferred1.await() + deferred2.await()
Log.e("執(zhí)行完畢" , result.toString())
}
Log.e("運(yùn)行時(shí)間" , time.toString())
}
打印結(jié)果如下:
開始運(yùn)行第二個(gè)協(xié)程
開始計(jì)算結(jié)果
子協(xié)程1運(yùn)行開始
子協(xié)程2運(yùn)行開始
returnNumber1: 調(diào)用了returnNumber1()方法
returnNumber1: 調(diào)用了returnNumber2()方法
執(zhí)行完畢: 3
運(yùn)行時(shí)間: 2009
下一節(jié),我們將用最有效的方式講解掛起函數(shù)的原理。文章來源地址http://www.zghlxwxcb.cn/news/detail-704495.html
到了這里,關(guān)于kotlin語(yǔ)法進(jìn)階 - 協(xié)程(一)協(xié)程基礎(chǔ)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!