Kotlin使用協(xié)程實(shí)現(xiàn)高效并發(fā)程序流程詳解
協(xié)程屬于Kotlin中非常有特色的一項(xiàng)技術(shù),因?yàn)榇蟛糠志幊陶Z(yǔ)言中是沒(méi)有協(xié)程這個(gè)概念的。那么什么是協(xié)程呢?它其實(shí)和線程有點(diǎn)相似,可以簡(jiǎn)單地將它理解成一種輕量級(jí)的線程。我們之前學(xué)習(xí)的線程是重量級(jí)的,它需要依靠操作系統(tǒng)的調(diào)度才能實(shí)現(xiàn)不同線程之間的切換,而使用協(xié)程卻可以?xún)H在編程語(yǔ)言的層面就能實(shí)現(xiàn)不同協(xié)程之間的切換,從而大大提示了并發(fā)編程的運(yùn)行效率。
比如我們有如下foo()和bar()方法:
fun foo(){
print(1)
print(2)
print(3)
}
fun bar(){
print(4)
print(5)
print(6)
}
在沒(méi)有開(kāi)啟線程的情況下,先后調(diào)用foo()和bar()這兩個(gè)方法,那么理論上的輸出結(jié)果一定是123456。而如果使用了協(xié)程,在協(xié)程A中去調(diào)用foo()方法,協(xié)程B中去調(diào)用bar()方法,雖然它們?nèi)匀粫?huì)運(yùn)行在同一個(gè)線程當(dāng)中,但是在執(zhí)行foo()方法時(shí)隨時(shí)都有可能被掛起轉(zhuǎn)而去執(zhí)行bar()方法,執(zhí)行bar()方法時(shí)也隨時(shí)都要可能被掛起而繼續(xù)執(zhí)行foo()方法,最終輸出結(jié)果也變得不確定了。
可以看出,協(xié)程允許我們?cè)趩尉€程模式下模擬多線程編程的效果,代碼執(zhí)行的掛起與恢復(fù)完全由編程語(yǔ)言來(lái)控制的,和操作系統(tǒng)無(wú)關(guān)。這種特性使得高并發(fā)程序的運(yùn)行效率得到了極大的提升。
1.協(xié)程的基本用法
Kotlin并沒(méi)有將協(xié)程納入標(biāo)準(zhǔn)庫(kù)的API中,而是以依賴(lài)庫(kù)的形式提供的。所以如果我們想使用協(xié)程功能,需要先在app/build.gradle文件中添加如下依賴(lài)庫(kù):
//協(xié)程
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
接下來(lái)創(chuàng)建一個(gè)coroutines.kt文件,并定義一個(gè)main()函數(shù)。
開(kāi)啟一個(gè)協(xié)程,最簡(jiǎn)單的方式就是使用Global.launch函數(shù),如下所示:
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
}
}
GlobalScope.launch 函數(shù)可以創(chuàng)建一個(gè)協(xié)程的作用域,這樣傳遞給launch函數(shù)的代碼塊(lambdal表達(dá)式)就是在協(xié)程中運(yùn)行的了,這里我們?cè)诖a塊中打印了一行日志。現(xiàn)在運(yùn)行main()函數(shù),結(jié)果并沒(méi)有輸出。
這是因?yàn)?,Global.launch函數(shù)每次創(chuàng)建的都是一個(gè)頂層協(xié)程,這種協(xié)程當(dāng)應(yīng)用程序運(yùn)行結(jié)束時(shí)也會(huì)跟著一起結(jié)束。剛才的日志之所以無(wú)法打印出來(lái),就是因?yàn)榇a塊中的代碼還沒(méi)來(lái)得及運(yùn)行,應(yīng)用程序就結(jié)束了。
要解決這個(gè)問(wèn)題,只需要讓程序延遲一段時(shí)間再結(jié)束就行了。這里使用Thread.sleep()方法讓主線程阻塞1秒鐘,現(xiàn)在重新運(yùn)行一下程序,你會(huì)發(fā)現(xiàn)日志可以正常打印了。
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
}
Thread.sleep(1000)
}

可是這種寫(xiě)法還是存在問(wèn)題,如果代碼塊中的代碼再1秒鐘之內(nèi)不能運(yùn)行結(jié)束,那么就會(huì)被強(qiáng)制中斷。
例如:
fun main() {
GlobalScope.launch {
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
Thread.sleep(1000)
}
我們?cè)诖a塊中加入了一個(gè)delay函數(shù),并在之后又打印了一行日志。delay()函數(shù)可以讓當(dāng)前協(xié)程延遲指定時(shí)間后再運(yùn)行,但它和Thread.sleep()方法不同。delay()函數(shù)是一個(gè)非阻塞式的掛起函數(shù),它只會(huì)掛起當(dāng)前協(xié)程,并不會(huì)影響其他協(xié)程的運(yùn)行。而Thread.sleep()方法會(huì)阻塞當(dāng)前的線程,這樣運(yùn)行在該線程下的所有協(xié)程都會(huì)被阻塞。注意,delay()函數(shù)只能在協(xié)程的作用域或其他掛起函數(shù)中調(diào)用。
這里我們讓協(xié)程掛起1.5秒,但是主線程卻只阻塞1秒,這樣重新運(yùn)行程序,你會(huì)發(fā)現(xiàn)代碼塊中新增的一條日志并沒(méi)有打印出來(lái),因?yàn)樗€沒(méi)來(lái)得及運(yùn)行,應(yīng)用程序就結(jié)束了。
借助runBlocking函數(shù)可以讓?xiě)?yīng)用程序在協(xié)程中所有代碼都運(yùn)行完了之后再結(jié)束。
fun main() {
runBlocking{
println("codes run in coroutine scope")
delay(1500)
println("codes run in coroutine scope finished")
}
}
runBlocking函數(shù)同樣會(huì)創(chuàng)建一個(gè)協(xié)程的作用域,但是它可以保證在協(xié)程作用域內(nèi)的所有代碼和子協(xié)程沒(méi)有全部執(zhí)行完之前一直阻塞當(dāng)前線程。需要注意的是,runBlocking函數(shù)通常只應(yīng)該再測(cè)試環(huán)境下使用,在正式環(huán)境中使用容易產(chǎn)生性能上的問(wèn)題。

可以看到,兩條日志都能夠正常打印出來(lái)了。
一旦設(shè)計(jì)高并發(fā)的應(yīng)用場(chǎng)景,協(xié)程相比于線程的優(yōu)勢(shì)就能體現(xiàn)出來(lái)了。
那么如何才能創(chuàng)建多個(gè)協(xié)程呢?使用launch函數(shù)就可以了,如下所示:
fun main() {
runBlocking {
launch {
println("launch1")
delay(1000)
println("launch1 finished")
}
launch {
println("launch2")
delay(1000)
println("launch2 finished")
}
}
}
這里的launch函數(shù)和我們剛才所使用的GlobalScope.launch函數(shù)不同。首先它必須在協(xié)程的作用域中才能調(diào)用,其次它會(huì)在當(dāng)前協(xié)程的作用域下創(chuàng)建子協(xié)程。子協(xié)程的特點(diǎn)是如果外層作用域的協(xié)程結(jié)束了,該作用域下的所有子協(xié)程也會(huì)一同結(jié)束。相比而言,GlobalScope.launch函數(shù)創(chuàng)建的永遠(yuǎn)是頂層協(xié)程,這一點(diǎn)和線程比較像,因?yàn)榫€程也沒(méi)有層級(jí)這一說(shuō),永遠(yuǎn)都是頂層的。
這里我們調(diào)用了兩次launch函數(shù),也就是創(chuàng)建了兩個(gè)子協(xié)程。運(yùn)行 程序結(jié)果如下:

可以看到,兩個(gè)子協(xié)程中的日志是交替打印的,說(shuō)明它們確實(shí)是像多線程那樣并發(fā)運(yùn)行的。然而這兩個(gè)子協(xié)程實(shí)際卻運(yùn)行在同一個(gè)線程當(dāng)中,只是由編程語(yǔ)言來(lái)決定如何在多個(gè)協(xié)程之間進(jìn)行調(diào)度,讓誰(shuí)運(yùn)行,讓誰(shuí)掛起。調(diào)度的過(guò)程完全不需要操作系統(tǒng)參與,這也就使得協(xié)程的并發(fā)效果很高。
例如:
fun main() {
val start = System.currentTimeMillis()
runBlocking {
repeat(100000){
launch {
println("測(cè)試")
}
}
}
val end=System.currentTimeMillis()
println(end-start)
}
這里使用repeat函數(shù)循環(huán)創(chuàng)建了10萬(wàn)個(gè)協(xié)程,僅僅耗時(shí)了564毫秒,如果開(kāi)啟的是線程可能已經(jīng)oom異常了。

不過(guò),隨著lanuch函數(shù)中的邏輯越來(lái)越復(fù)雜,可能你需要將部分代碼提取到一個(gè)單獨(dú)的函數(shù)中。這個(gè)時(shí)候就會(huì)出現(xiàn)一個(gè)問(wèn)題,我們?cè)賚aunch函數(shù)中編寫(xiě)的代碼是擁有協(xié)程作用域的,但是提取到一個(gè)單獨(dú)函數(shù)中就沒(méi)有協(xié)程作用域了。

那么我們應(yīng)該如何調(diào)用像delay()這樣的掛起函數(shù)呢?Kotlin提供了一個(gè)suspend關(guān)鍵字,使用它可以將任意函數(shù)聲明成掛起函數(shù),而掛起函數(shù)之間都是可以互相調(diào)用的,如下所示:
suspend fun printDot(){
println(".")
delay(1000)
}
這樣就可以在printDot()函數(shù)中調(diào)用delay()函數(shù)了
但是,suspend關(guān)鍵字只能將一個(gè)函數(shù)聲明成掛起函數(shù),是無(wú)法給它提供協(xié)程作用域的。比如你現(xiàn)在嘗試在printDot()函數(shù)中調(diào)用launch函數(shù),一定是無(wú)法調(diào)用成功的,因?yàn)閘aunch函數(shù)要求必須在協(xié)程作用域當(dāng)中才能調(diào)用。
這個(gè)問(wèn)題可以借助coroutineScope函數(shù)來(lái)解決。coroutineScope函數(shù)也是一個(gè)掛起函數(shù),因此可以在任何其他掛起函數(shù)中調(diào)用。它的特點(diǎn)是會(huì)繼承外部的協(xié)程作用域并創(chuàng)建一個(gè)子作用域,借助這個(gè)特性,我們就可以給任意掛起函數(shù)提供協(xié)程作用域了。示例寫(xiě)法如下:
suspend fun printDot()= coroutineScope {
launch {
println(".")
delay(1000)
}
}
可以看到,現(xiàn)在我們就可以在printDot()這個(gè)掛起函數(shù)中調(diào)用launch函數(shù)了。
另外,coroutineScope函數(shù)和runBlocking函數(shù)還有點(diǎn)類(lèi)似,它可以保證其作用域內(nèi)的所有代碼和子協(xié)程在全部執(zhí)行完之前,會(huì)一直阻塞當(dāng)前協(xié)程。
fun main() {
runBlocking {
coroutineScope {
launch {
for(i in 1..10){
println(i)
delay(1000)
}
}
}
println("coroutineScope finished")
}
println("runBlocking finished")
}
這里先使用runBlocking函數(shù)創(chuàng)建了一個(gè)協(xié)程作用域,然后又使用coroutineScope 函數(shù)創(chuàng)建了一個(gè)子協(xié)程作用域。在coroutineScope 的作用域中,我們調(diào)用launch函數(shù)創(chuàng)建了一個(gè)子協(xié)程,并通過(guò)for循環(huán)依次打印數(shù)字1到10,每次打印間隔一秒鐘。最后在runBlocking和corountineScope函數(shù)的結(jié)尾,分別又打印了一行日志。

由此可見(jiàn),corountineScope函數(shù)確實(shí)是將協(xié)程阻塞住了,只有當(dāng)他作用域內(nèi)的所有代碼和子協(xié)程都執(zhí)行完畢之后,corountineScope函數(shù)之后的代碼才能得到運(yùn)行。
corountineScope函數(shù)只會(huì)阻塞當(dāng)前協(xié)程,既不影響當(dāng)前協(xié)程,也不影響任何線程,因此是不會(huì)造成性能上的問(wèn)題的。而runBlocking函數(shù)由于會(huì)阻塞當(dāng)前線程,如果你恰好又在主線程當(dāng)中調(diào)用它的話中調(diào)用它的話,那么就有可能導(dǎo)致界面卡死的情況,所以不太推薦在實(shí)際項(xiàng)目中使用。
2.更多的作用域構(gòu)建器
前面學(xué)習(xí)了GlobalScope.launch,runBlocking,launch,corountineScope這幾種作用域構(gòu)建器,它們都可以用于創(chuàng)建一個(gè)新的協(xié)程作用域。不管GlobalScope.launch和runBlocking函數(shù)都可以在任意地方調(diào)用的,corountineScope函數(shù)可以在協(xié)程作用域或掛起函數(shù)在調(diào)用,而launch函數(shù)只能在協(xié)程作用域中調(diào)用。而launch函數(shù)只能在協(xié)程作用域中調(diào)用。
runBlocking由于會(huì)阻塞線程,因此只建議在測(cè)試環(huán)境下使用。而GlobalScope.launch由于每次創(chuàng)建的都是頂層協(xié)程,一般也不太建議使用。因?yàn)楣芾沓杀颈容^高,比如我們?cè)倌硞€(gè)Activity中使用協(xié)程發(fā)起了一條網(wǎng)絡(luò)請(qǐng)求,由于網(wǎng)絡(luò)請(qǐng)求是耗時(shí)的,用戶(hù)在服務(wù)器還沒(méi)來(lái)得及響應(yīng)的情況下就關(guān)閉了當(dāng)前Activity,此時(shí)應(yīng)該取消這條網(wǎng)絡(luò)請(qǐng)求,或者至少不應(yīng)該進(jìn)行回調(diào),因?yàn)锳ctivity已經(jīng)不存在了,回調(diào)也沒(méi)意義。
那么協(xié)程要怎樣取消呢?不管是GlobalScope.launch函數(shù)還是launch函數(shù),它們都會(huì)返回一個(gè)job對(duì)象,只需要調(diào)用job對(duì)象的cancel()方法就可以取消協(xié)程了,如下所示:
val job= GlobalScope.launch {
//處理具體的邏輯
}
job.cancel()
但是如果我們每次創(chuàng)建的都是頂層協(xié)程,那么當(dāng)Activity關(guān)閉時(shí),就需要逐個(gè)調(diào)用所有已創(chuàng)建的協(xié)程的cancel()方法。這樣管理成本太大。
因此,GlobalScope.launch這種協(xié)程作用域構(gòu)建器,在實(shí)際項(xiàng)目中不太常用。實(shí)際項(xiàng)目中比較常用的寫(xiě)法:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
//處理具體的邏輯
}
job.cancel()
我們先創(chuàng)建了一個(gè)Job對(duì)象,然后把它傳入CoroutineScope()函數(shù)當(dāng)中,CoroutineScope()函數(shù)會(huì)返回一個(gè)CoroutineScope對(duì)象,有了CoroutineScope對(duì)象之后,我們就可以隨時(shí)調(diào)用它的lanuch函數(shù)來(lái)創(chuàng)建一個(gè)協(xié)程了。
現(xiàn)在所有調(diào)用CoroutineScope的launch 函數(shù)所創(chuàng)建的協(xié)程,都會(huì)被關(guān)聯(lián)在Job對(duì)象的作用域下面。這樣只需要調(diào)用一次cancel()方法,就可以將同一作用域內(nèi)的所有協(xié)程全部取消,從而大大降低協(xié)程管理的成本。
我們已經(jīng)指定了調(diào)用launch函數(shù)可以創(chuàng)建一個(gè)新的協(xié)程,但是launch函數(shù)只能用于執(zhí)行一段邏輯,卻不能獲取執(zhí)行的結(jié)果,因?yàn)樗姆祷刂涤肋h(yuǎn)是一個(gè)Job對(duì)象。而我們使用async函數(shù)就可以實(shí)現(xiàn)創(chuàng)建一個(gè)協(xié)程并獲取它的執(zhí)行結(jié)果。
async函數(shù)必須在協(xié)程作用域當(dāng)中才能調(diào)用,它會(huì)創(chuàng)建一個(gè)新的子協(xié)程并返回一個(gè)Deferred對(duì)象,如果我們想要獲取async函數(shù)代碼塊的執(zhí)行結(jié)果,只需要調(diào)用Deferred對(duì)象的await()方法即可,代碼如下所示:
val job = Job()
val scope = CoroutineScope(job)
scope.launch {
//處理具體的邏輯
}
job.cancel()

事實(shí)上,在調(diào)用了async函數(shù)之后,代碼塊中的代碼就會(huì)立刻開(kāi)始執(zhí)行。當(dāng)調(diào)用await()方法時(shí),如果代碼塊中的代碼還沒(méi)執(zhí)行完,那么await()方法會(huì)將當(dāng)前協(xié)程阻塞住,直到可以獲得async函數(shù)的執(zhí)行結(jié)果。例如:
runBlocking {
val start=System.currentTimeMillis()
val result1=async {
delay(1000)
5+5
}.await()
val result2=async {
delay(1000)
4+6
}.await()
println("result is ${result1+result2}")
val end=System.currentTimeMillis()
println("cost ${end-start} ms.")
}
這里連續(xù)使用了兩個(gè)async函數(shù)來(lái)執(zhí)行任務(wù),并在代碼塊中調(diào)用delay()方法進(jìn)行1秒的延遲。而且await()方法在async函數(shù)代碼塊中的代碼執(zhí)行完之前會(huì)一直將當(dāng)前協(xié)程阻塞住。

可以看到整段代碼耗時(shí)是2032毫秒,說(shuō)明這里的兩個(gè)async函數(shù)確實(shí)是一種串行的關(guān)系,前一個(gè)執(zhí)行完了后一個(gè)才能執(zhí)行。
兩個(gè)async函數(shù)完全可以同時(shí)執(zhí)行從而提高效率,對(duì)上述代碼進(jìn)行修改
runBlocking {
val start=System.currentTimeMillis()
val result1=async {
delay(1000)
5+5
}.await()
val result2=async {
delay(1000)
4+6
}.await()
println("result is ${result1+result2}")
val end=System.currentTimeMillis()
println("cost ${end-start} ms.")
}
現(xiàn)在我們不在每次調(diào)用async函數(shù)之后就立刻使用await()方法獲取結(jié)果了,而是僅在需要用到async函數(shù)的執(zhí)行結(jié)果時(shí)才調(diào)用await()方法進(jìn)行獲取,這樣兩個(gè)async函數(shù)就變成一種并行關(guān)系了。

可以看到代碼運(yùn)行耗時(shí)變成了1027毫秒,運(yùn)行效率提升。
還有一個(gè)比較特殊的作用域構(gòu)建器:withContext()函數(shù)。withContext()函數(shù)是一個(gè)掛起函數(shù),大體可以將它理解成async函數(shù)的一種簡(jiǎn)化版寫(xiě)法,示例寫(xiě)法如下:
runBlocking {
val result=withContext(Dispatchers.Default){
6+5
}
println(result)
}
調(diào)用withContext()函數(shù)之后,會(huì)立即執(zhí)行代碼塊中的代碼,同時(shí)將當(dāng)前協(xié)程阻塞住。當(dāng)代碼塊中的代碼全部執(zhí)行完之后,會(huì)將最后一行的執(zhí)行結(jié)果作為withContext()函數(shù)的返回值返回,因此基本上相當(dāng)于val result=async{5+5}.await()的寫(xiě)法。唯一不同的是,withContext()函數(shù)強(qiáng)制要求我們指定一個(gè)線程參數(shù)。
線程參數(shù)主要有以下3種值可選:Dispatchers.Default,Dispatchers.IO,Dispatchers.Main。Dispatchers.Default表示會(huì)使用一種默認(rèn)低并發(fā)的線程策略,當(dāng)你要執(zhí)行的代碼屬于計(jì)算密集型任務(wù)時(shí),開(kāi)啟過(guò)高的并發(fā)反而可能會(huì)影響任務(wù)的運(yùn)行效率,此時(shí)就可以使用Dispatchers.Default。Dispatchers.IO表示會(huì)使用一種較高并發(fā)的線程策略,當(dāng)你要執(zhí)行的代碼大多數(shù)時(shí)間是在阻塞和等待中,比如說(shuō)執(zhí)行網(wǎng)絡(luò)請(qǐng)求時(shí),為了能夠支持更高的并發(fā)數(shù)量,此時(shí)就可以使用Dispatchers.IO。Dispatchers.Main則表示不會(huì)開(kāi)啟子線程,而是在Android主線程中執(zhí)行代碼,但是這個(gè)值只能在Android項(xiàng)目中使用。
3.使用協(xié)程簡(jiǎn)化回調(diào)的寫(xiě)法
通過(guò)回調(diào)機(jī)制實(shí)現(xiàn)獲取異步網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)響應(yīng)的功能?;卣{(diào)機(jī)制基本上是依靠匿名類(lèi)來(lái)實(shí)現(xiàn)的,但是匿名類(lèi)的寫(xiě)法通常比較繁瑣,比如如下代碼:
HttpUtil.sendHttpRequest(address,object:HttpCallbackListener){
override fun onFinish(response:String){
//得到服務(wù)器返回的具體內(nèi)容
}
override fun onError(e:Exception){
//這里對(duì)異常情況進(jìn)行處理
}
})
在多少個(gè)地方發(fā)起網(wǎng)絡(luò)請(qǐng)求,就需要編寫(xiě)多少次這樣的匿名類(lèi)實(shí)現(xiàn)。有沒(méi)有更簡(jiǎn)單的寫(xiě)法呢?Kotlin的協(xié)程使我們的這種設(shè)想成為了可能,只需要借助SuspendCoroutine函數(shù)就能將傳統(tǒng)回調(diào)機(jī)制的寫(xiě)法大幅簡(jiǎn)化。
SuspendCoroutine函數(shù)必須在協(xié)程作用域或掛起函數(shù)才能調(diào)用,它接收一個(gè)Lambda表達(dá)式參數(shù),主要作用是將當(dāng)前協(xié)程立即掛起,然后在一個(gè)普通的線程中執(zhí)行Lambda表達(dá)式中的代碼。Lambda表達(dá)式的參數(shù)列表上會(huì)傳入一個(gè)Continuation參數(shù),調(diào)用它的resume()方法或resumeWithException()可以讓協(xié)程恢復(fù)執(zhí)行。
接下來(lái)我們借助這個(gè)函數(shù)對(duì)傳統(tǒng)的回調(diào)寫(xiě)法進(jìn)行優(yōu)化。首先定義一個(gè)request()函數(shù),代碼如下:
suspend fun request(address:String):String{
return suspendCoroutine{continuation ->
HttpUtil.sendHttpRequest(address,object:HttpCallbackListener{
override fun onFinish(response:String){
continuation.resume(response)
}
override fun onError(e:Exception){
continuation.resumeWithException(e)
}
})
}
}
可以看到,request()函數(shù)是一個(gè)掛起函數(shù),并且接收一個(gè)address參數(shù)。在request()函數(shù)的內(nèi)部,我們調(diào)用了suspendCoroutine函數(shù),這樣當(dāng)前協(xié)程就會(huì)被立即掛起,而Lambda表達(dá)式中的代碼則會(huì)在普通線程中執(zhí)行。接下來(lái)我們?cè)貺ambda表達(dá)式中調(diào)用HttpUtil.sendHttpRequest()方法發(fā)起網(wǎng)絡(luò)請(qǐng)求,并通過(guò)傳統(tǒng)回調(diào)的方式監(jiān)聽(tīng)請(qǐng)求結(jié)果。如果成功就調(diào)用continuation的resume()方法恢復(fù)掛起的協(xié)程,并傳入服務(wù)器響應(yīng)的數(shù)據(jù),該值會(huì)成為suspendCoroutine函數(shù)的返回值。如果請(qǐng)求失敗,就調(diào)用Continuation的resumeWithException()恢復(fù)被掛起的協(xié)程,并傳入具體的異常原因。
這樣不管之后我們要發(fā)起多少次網(wǎng)絡(luò)請(qǐng)求,都不需要再重復(fù)進(jìn)行回調(diào)實(shí)現(xiàn)了。比如說(shuō)獲取百度頁(yè)面的響應(yīng)數(shù)據(jù),就可以這樣寫(xiě):
suspend fun getBaiduResponse(){
try{
val response=request("https://www.baidu.com/")
//對(duì)服務(wù)器響應(yīng)的數(shù)據(jù)進(jìn)行處理
}catch(e:Exception){
//對(duì)異常情況進(jìn)行處理
}
}
由于getBaiduResponse()是一個(gè)掛起函數(shù),因此當(dāng)它調(diào)用了request()函數(shù)時(shí),當(dāng)前的協(xié)程就已經(jīng)被立刻掛起,然后一直等待網(wǎng)絡(luò)請(qǐng)求成功或失敗后,當(dāng)前協(xié)程才能恢復(fù)運(yùn)行。這樣即使不使用回調(diào)的寫(xiě)法,我們也能夠獲得異步網(wǎng)絡(luò)請(qǐng)求的響應(yīng)數(shù)據(jù),而如果請(qǐng)求失敗,則會(huì)進(jìn)入catch語(yǔ)句當(dāng)中。
事實(shí)上,suspendCoroutine函數(shù)幾乎可以用于簡(jiǎn)化任何回調(diào)的寫(xiě)法,比如之前使用Retrofit來(lái)發(fā)起網(wǎng)絡(luò)請(qǐng)求需要這樣寫(xiě):
val appService=ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object:Callback<List<App>>{
override fun onResponse(call:Call<List<App>>,response:Response<List<App>>){
//得到服務(wù)器返回的數(shù)據(jù)
}
override fun onFailure(call:Call<List<App>>,t:Throwable){
//在這里對(duì)異常情況進(jìn)行處理
}
})
使用suspendCoroutine函數(shù),就可以對(duì)上述寫(xiě)法進(jìn)行大幅度的簡(jiǎn)化。
由于不同的接口返回的數(shù)據(jù)類(lèi)型不同,所以我們要使用泛型的方式,定義一個(gè)await()函數(shù),代碼如下所示:
suspend fun <T> Call<T>.await():T{
return suspendCoroutine{continuation->
enqueue(object:Callback<T>{
override fun onResponse(call:Call<T>,response:Response<T>){
val body=response.body()
if(body!=null){
continuation.resume(body)
else continuation.resumeWithException(RuntimeException("response body is null"))
}
override fun onFailure(call:Call<T>,t:Throwable){
continuation.resumeWithException(t)
}
})
}
}
首先await()函數(shù)仍然是一個(gè)掛起函數(shù),然后我們給它聲明了一個(gè)泛型T,并將await()函數(shù)定義成Call< T >的擴(kuò)展函數(shù),這樣所有返回值是Call類(lèi)型的Retrofit網(wǎng)絡(luò)請(qǐng)求接口就都可以直接調(diào)用await()函數(shù)了。
await()函數(shù)使用了suspendCoroutine函數(shù)來(lái)掛起當(dāng)前協(xié)程,并且由于擴(kuò)展函數(shù)的原因,我們現(xiàn)在擁有了Call對(duì)象的上下文,那么這里就可以直接調(diào)用enqueue()方法讓Retrofit發(fā)起網(wǎng)絡(luò)請(qǐng)求。使用同樣的方式對(duì)Retrofit響應(yīng)的數(shù)據(jù)或網(wǎng)絡(luò)請(qǐng)求失敗的情況進(jìn)行處理。
有了await()函數(shù)之后,我們調(diào)用可以變得更簡(jiǎn)便,如下:
suspend fun getAppData(){
try{
val appList=ServiceCreator.create<AppService>().getAppData().await()
//對(duì)服務(wù)器響應(yīng)的數(shù)據(jù)進(jìn)行處理
}catch(e:Exception){
//對(duì)異常情況進(jìn)行處理
}
}
這樣只需要簡(jiǎn)單調(diào)用await()函數(shù)就可以讓Retrofit發(fā)起網(wǎng)絡(luò)請(qǐng)求,并直接獲得服務(wù)器響應(yīng)的數(shù)據(jù)。當(dāng)然每次網(wǎng)絡(luò)請(qǐng)求都要進(jìn)行一次try catch處理也比較麻煩,在不處理的情況下,如果發(fā)生異常會(huì)一層層往上拋出,一直到被某一層函數(shù)處理位置,因此我們可以統(tǒng)一一個(gè)入口函數(shù)只進(jìn)行一次try catch。
到此這篇關(guān)于Kotlin使用協(xié)程實(shí)現(xiàn)高效并發(fā)程序流程詳解的文章就介紹到這了,更多相關(guān)Kotlin協(xié)程實(shí)現(xiàn)并發(fā)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android自定義控件實(shí)現(xiàn)時(shí)間軸
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)時(shí)間軸,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04
Android開(kāi)發(fā)實(shí)現(xiàn)去除bitmap無(wú)用白色邊框的方法示例
這篇文章主要介紹了Android開(kāi)發(fā)實(shí)現(xiàn)去除bitmap無(wú)用白色邊框的方法,結(jié)合實(shí)例形式給出了Android去除bitmap無(wú)用白色邊框的具體操作步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-11-11
Flutter倒計(jì)時(shí)/計(jì)時(shí)器的實(shí)現(xiàn)代碼
這篇文章主要介紹了Flutter倒計(jì)時(shí)/計(jì)時(shí)器的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
Android 5.0+ 屏幕錄制實(shí)現(xiàn)的示例代碼
這篇文章主要介紹了Android 5.0+ 屏幕錄制實(shí)現(xiàn)的示例代碼,從 5.0 開(kāi)始,系統(tǒng)提供給了 app 錄制屏幕的一系列方法,不需要 root 權(quán)限,只需要用戶(hù)授權(quán)即可錄屏,相對(duì)來(lái)說(shuō)較為簡(jiǎn)單,感興趣的小伙伴們可以參考一下2018-05-05
Android編程之高效開(kāi)發(fā)App的10個(gè)建議
這篇文章主要介紹了Android編程之高效開(kāi)發(fā)App的10個(gè)建議,較為詳細(xì)的分析了Android開(kāi)發(fā)中的常見(jiàn)問(wèn)題與注意事項(xiàng),具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Android中AlertDialog四種對(duì)話框的最科學(xué)編寫(xiě)用法(實(shí)例代碼)
這篇文章主要介紹了Android中AlertDialog四種對(duì)話框的最科學(xué)編寫(xiě)用法,本文通過(guò)代碼講解的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11
如何判斷軟件程序是否聯(lián)網(wǎng) 聯(lián)網(wǎng)狀態(tài)提示信息Android實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了如何判斷軟件程序是否聯(lián)網(wǎng)的實(shí)現(xiàn)代碼,Android實(shí)現(xiàn)聯(lián)網(wǎng)狀態(tài)信息提示,感興趣的小伙伴們可以參考一下2016-05-05
Android編程之菜單Menu的創(chuàng)建方法示例
這篇文章主要介紹了Android編程之菜單Menu的創(chuàng)建方法,結(jié)合實(shí)例形式分析了Android菜單Menu的布局、響應(yīng)及功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-08-08
Qt5.12.6配置Android Arm開(kāi)發(fā)環(huán)境(圖文)
本文主要介紹了Qt5.12.6配置Android Arm開(kāi)發(fā)環(huán)境,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06

