Kotlin使用協(xié)程編寫(xiě)高效的并發(fā)程序

? ? ? ? 協(xié)程屬于Kotlin中非常有特色的一項(xiàng)技術(shù),因?yàn)榇蟛糠志幊陶Z(yǔ)言中是沒(méi)有協(xié)程這個(gè)概念的锡凝。

? ? ? ? 那么什么是協(xié)程呢粘昨?它其實(shí)和線程是有點(diǎn)類(lèi)似的,可以簡(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)行效率放刨。

舉一個(gè)具體點(diǎn)的例子工秩,比如我們有如下foo()和bar()兩個(gè)方法:

????fun foo() {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? fun bar() {

????????a()? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? x()

? ? ? ? b()? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? y()

????????c()? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? z()

????}? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?}

? ? ? ?在沒(méi)有開(kāi)啟線程的情況下,先后調(diào)用foo()和bar()這兩個(gè)方法进统,那么理論上結(jié)果一定是a()助币、b()、c()執(zhí)行完了以后螟碎,x()眉菱、y()、z()才能夠得到執(zhí)行掉分。而如果使用了協(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í)都有可能被掛起轉(zhuǎn)而繼續(xù)執(zhí)行foo()方法,最終的輸出結(jié)果也就變得不確定了惜姐。

? ? ? ?可以看出犁跪,協(xié)程允許我們?cè)趩尉€程模式下模擬多線程編程的效果,代碼執(zhí)行時(shí)的掛起與恢復(fù)完全是由編程語(yǔ)言來(lái)控制的歹袁,和操作系統(tǒng)無(wú)關(guān)坷衍。這種特性使得高并發(fā)程序的運(yùn)行效率得到了極大的提升,試想一下条舔,開(kāi)啟10萬(wàn)個(gè)線程完全是不可想象的事吧枫耳?而開(kāi)啟10萬(wàn)個(gè)協(xié)程就是完全可行的,待會(huì)我們就會(huì)對(duì)這個(gè)功能進(jìn)行驗(yàn)證孟抗。

現(xiàn)在你已經(jīng)了解了協(xié)程的一些基本概念嘉涌,那么接下來(lái)我們就開(kāi)始學(xué)習(xí)Kotlin中協(xié)程的用法。

協(xié)程的基本用法

????????Kotlin并沒(méi)有將協(xié)程納入標(biāo)準(zhǔn)庫(kù)的API當(dāng)中夸浅,而是以依賴(lài)庫(kù)的形式提供的仑最。所以如果我們想要使用協(xié)程功能,需要先在app/build.gradle文件當(dāng)中添加如下依賴(lài)庫(kù):

dependencies {

????implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"

????implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"

}

????????第二個(gè)依賴(lài)庫(kù)是在Android項(xiàng)目中才會(huì)用到的帆喇,本節(jié)我們編寫(xiě)的代碼示例都是純Kotlin程序警医,所以其實(shí)用不到第二個(gè)依賴(lài)庫(kù)。但為了下次在Android項(xiàng)目中使用協(xié)程時(shí)不再單獨(dú)進(jìn)行說(shuō)明坯钦,這里就一同引入進(jìn)來(lái)了预皇。

????????接下來(lái)創(chuàng)建一個(gè)CoroutinesTest.kt文件,并定義一個(gè)main()函數(shù)婉刀,然后開(kāi)始我們的協(xié)程之旅吧吟温。

????????首先我們要面臨的第一個(gè)問(wèn)題就是,如何開(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ù)的代碼塊(Lambda表達(dá)式)就是在協(xié)程中運(yùn)行的了律秃,這里我們只是在代碼塊中打印了一行日志爬橡。那么現(xiàn)在運(yùn)行main()函數(shù),日志能成功打印出來(lái)嗎棒动?如果你嘗試一下糙申,會(huì)發(fā)現(xiàn)沒(méi)有任何日志輸出。

????????這是因?yàn)榇遥珿lobal.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)題也很簡(jiǎn)單,我們讓程序延遲一段時(shí)間再結(jié)束就行了衔统,如下所示:

fun main() {

????GlobalScope.launch {

????????println("codes run in coroutine scope")

????}

????Thread.sleep(1000)

}

這里使用Thread.sleep()方法讓主線程阻塞1秒鐘鹿榜,現(xiàn)在重新運(yùn)行程序,你會(huì)發(fā)現(xiàn)日志可以正常打印出來(lái)了锦爵,如圖所示舱殿。


在協(xié)程中打印日志

????????可是這種寫(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秒眶掌,最終會(huì)是什么結(jié)果呢?重新運(yùn)行程序巴碗,你會(huì)發(fā)現(xiàn)代碼塊中新增的一條日志并沒(méi)有打印出來(lái)朴爬,因?yàn)樗€沒(méi)能來(lái)得及運(yùn)行,應(yīng)用程序就已經(jīng)結(jié)束了橡淆。

????????那么有沒(méi)有什么辦法能讓?xiě)?yīng)用程序在協(xié)程中所有代碼都運(yùn)行完了之后再結(jié)束呢召噩?當(dāng)然也是有的,借助runBlocking函數(shù)就可以實(shí)現(xiàn)這個(gè)功能:

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)題。

????????現(xiàn)在重新運(yùn)行程序施绎,結(jié)果如圖所示溯革。


runBlocking函數(shù)的運(yùn)行效果

????????可以看到贞绳,兩條日志都能夠正常打印出來(lái)了。

????????雖說(shuō)現(xiàn)在我們已經(jīng)能夠讓代碼在協(xié)程中運(yùn)行了致稀,可是好像并沒(méi)有體會(huì)到什么特別的好處冈闭。這是因?yàn)槟壳八械拇a都是運(yùn)行在同一個(gè)協(xié)程當(dāng)中的,而一旦涉及高并發(fā)的應(yīng)用場(chǎng)景抖单,協(xié)程相比于線程的優(yōu)勢(shì)就能體現(xiàn)出來(lái)了萎攒。

????????那么如何才能創(chuàng)建多個(gè)協(xié)程呢?很簡(jiǎn)單矛绘,使用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é)程并發(fā)運(yùn)行的效果

????????可以看到怔昨,兩個(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ā)效率會(huì)出奇得高罩锐。

????????那么具體會(huì)有多高呢奉狈?我們來(lái)做下實(shí)驗(yàn)就知道了,代碼如下所示:

fun main() {

????val start = System.currentTimeMillis()

????runBlocking {

????????repeat(100000) {

????????launch {

????????println(".")

????}

}}

????val end = System.currentTimeMillis()

????println(end - start)}

????????這里使用repeat函數(shù)循環(huán)創(chuàng)建了10萬(wàn)個(gè)協(xié)程涩惑,不過(guò)在協(xié)程當(dāng)中并沒(méi)有進(jìn)行什么有意義的操作仁期,只是象征性地打印了一個(gè)點(diǎn),然后記錄一下整個(gè)操作的運(yùn)行耗時(shí)。現(xiàn)在重新運(yùn)行一下程序跛蛋,結(jié)果如圖所示熬的。


10萬(wàn)個(gè)協(xié)程并發(fā)的運(yùn)行效率

????????可以看到,這里僅僅耗時(shí)了961毫秒赊级,這足以證明協(xié)程有多么高效押框。試想一下,如果開(kāi)啟的是10萬(wàn)個(gè)線程理逊,程序或許已經(jīng)出現(xiàn)OOM異常了橡伞。

????????不過(guò),隨著launch函數(shù)中的邏輯越來(lái)越復(fù)雜晋被,可能你需要將部分代碼提取到一個(gè)單獨(dú)的函數(shù)中骑歹。這個(gè)時(shí)候就產(chǎn)生了一個(gè)問(wèn)題:我們?cè)趌aunch函數(shù)中編寫(xiě)的代碼是擁有協(xié)程作用域的,但是提取到一個(gè)單獨(dú)的函數(shù)中就沒(méi)有協(xié)程作用域了墨微,那么我們?cè)撊绾握{(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è)子協(xié)程舀奶,借助這個(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í)行完之前腹躁,外部的協(xié)程會(huì)一直被掛起。我們來(lái)看如下示例代碼:

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é)程作用域南蓬,然后調(diào)用coroutineScope函數(shù)創(chuàng)建了一個(gè)子協(xié)程纺非。在coroutineScope的作用域中哑了,我們又調(diào)用launch函數(shù)創(chuàng)建了一個(gè)子協(xié)程,并通過(guò)for循環(huán)依次打印數(shù)字1到10铐炫,每次打印間隔一秒鐘垒手。最后在runBlocking和coroutineScope函數(shù)的結(jié)尾蒜焊,分別又打印了一行日志〉剐牛現(xiàn)在重新運(yùn)行一下程序,結(jié)果如圖所示泳梆。


coroutineScope函數(shù)的運(yùn)行效果

????????你會(huì)看到鳖悠,控制臺(tái)會(huì)以1秒鐘的間隔依次輸出數(shù)字1到10,然后才會(huì)打印coroutineScope函數(shù)結(jié)尾的日志优妙,最后打印runBlocking函數(shù)結(jié)尾的日志乘综。

????????由此可見(jiàn),coroutineScope函數(shù)確實(shí)是將外部協(xié)程掛起了套硼,只有當(dāng)它作用域內(nèi)的所有代碼和子協(xié)程都執(zhí)行完畢之后卡辰,coroutineScope函數(shù)之后的代碼才能得到運(yùn)行。

????????雖然看上去coroutineScope函數(shù)和runBlocking函數(shù)的作用是有點(diǎn)類(lèi)似的邪意,但是coroutineScope函數(shù)只會(huì)阻塞當(dāng)前協(xié)程九妈,既不影響其他協(xié)程,也不影響任何線程雾鬼,因此是不會(huì)造成任何性能上的問(wèn)題的萌朱。而runBlocking函數(shù)由于會(huì)掛起外部線程,如果你恰好又在主線程中當(dāng)中調(diào)用它的話(huà)策菜,那么就有可能會(huì)導(dǎo)致界面卡死的情況晶疼,所以不太推薦在實(shí)際項(xiàng)目中使用。

????????好了又憨,現(xiàn)在我們就將協(xié)程的基本用法都學(xué)習(xí)完了翠霍,你也算是已經(jīng)成功入門(mén)了。那么接下來(lái)蠢莺,就讓我們開(kāi)始學(xué)習(xí)協(xié)程更多的知識(shí)吧壶运。

更多的作用域構(gòu)建器

????????在上一小節(jié)中,我們學(xué)習(xí)了GlobalScope.launch浪秘、runBlocking蒋情、launch、coroutineScope這幾種作用域構(gòu)建器耸携,它們都可以用于創(chuàng)建一個(gè)新的協(xié)程作用域棵癣。不過(guò)GlobalScope.launch和runBlocking函數(shù)是可以在任意地方調(diào)用的,coroutineScope函數(shù)可以在協(xié)程作用域或掛起函數(shù)中調(diào)用夺衍,而launch函數(shù)只能在協(xié)程作用域中調(diào)用狈谊。

????????前面已經(jīng)說(shuō)了,runBlocking由于會(huì)阻塞線程,因此只建議在測(cè)試環(huán)境下使用河劝。而GlobalScope.launch由于每次創(chuàng)建的都是頂層協(xié)程壁榕,一般也不太建議使用,除非你非常明確就是要?jiǎng)?chuàng)建頂層協(xié)程赎瞎。

????????為什么說(shuō)不太建議使用頂層協(xié)程呢牌里?主要還是因?yàn)樗芾砥饋?lái)成本太高了。舉個(gè)例子务甥,比如我們?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í)按理說(shuō)應(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()方法,試想一下裆操,這樣的代碼是不是根本無(wú)法維護(hù)怒详?

????????因此,GlobalScope.launch這種協(xié)程作用域構(gòu)建器踪区,在實(shí)際項(xiàng)目中也是不太常用的昆烁。下面我來(lái)演示一下實(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()是個(gè)函數(shù),雖然它的命名更像是一個(gè)類(lèi)传泊。CoroutineScope()函數(shù)會(huì)返回一個(gè)CoroutineScope對(duì)象鼠渺,這種語(yǔ)法結(jié)構(gòu)的設(shè)計(jì)更像是我們創(chuàng)建了一個(gè)CoroutineScope的實(shí)例,可能也是Kotlin有意為之的眷细。有了CoroutineScope對(duì)象之后拦盹,就可以隨時(shí)調(diào)用它的launch函數(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é)程管理的成本沼侣。

????????不過(guò)相比之下祖能,CoroutineScope()函數(shù)更適合用于實(shí)際項(xiàng)目當(dāng)中,如果只是在main()函數(shù)中編寫(xiě)一些學(xué)習(xí)測(cè)試用的代碼蛾洛,還是使用runBlocking函數(shù)最為方便养铸。

????????協(xié)程的內(nèi)容確實(shí)比較多,下面我們還要繼續(xù)學(xué)習(xí)雅潭。你已經(jīng)知道了調(diào)用launch函數(shù)可以創(chuàng)建一個(gè)新的協(xié)程揭厚,但是launch函數(shù)只能用于執(zhí)行一段邏輯却特,卻不能獲取執(zhí)行的結(jié)果扶供,因?yàn)樗姆祷刂涤肋h(yuǎn)是一個(gè)Job對(duì)象。那么有沒(méi)有什么辦法能夠創(chuàng)建一個(gè)協(xié)程并獲取它的執(zhí)行結(jié)果呢裂明?當(dāng)然有椿浓,使用async函數(shù)就可以實(shí)現(xiàn)。

????????async函數(shù)必須在協(xié)程作用域當(dāng)中才能調(diào)用闽晦,它會(huì)創(chuàng)建一個(gè)新的子協(xié)程并返回一個(gè)Deferred對(duì)象扳碍,如果我們想要獲取async函數(shù)代碼塊的執(zhí)行結(jié)果,只需要調(diào)用Deferred對(duì)象的await()方法即可仙蛉,代碼如下所示:

fun main() {

????runBlocking {

????????val result = async {

????????????5 + 5

????????}.await()

????????println(result)

????}

}

????????這里我們?cè)赼sync函數(shù)的代碼塊中進(jìn)行了一個(gè)簡(jiǎn)單的數(shù)學(xué)運(yùn)算笋敞,然后調(diào)用await()方法獲取運(yùn)算結(jié)果,最終將結(jié)果打印出來(lái)荠瘪。重新運(yùn)行一下代碼夯巷,結(jié)果如圖所示。


打印async函數(shù)的執(zhí)行結(jié)果

????????不過(guò)async函數(shù)的奧秘還不止于此哀墓。事實(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é)果臀突。

為了證實(shí)這一點(diǎn),我們編寫(xiě)如下代碼進(jìn)行驗(yàn)證:

fun main() {

????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é)程阻塞住磕瓷,那么為了便于驗(yàn)證盒齿,我們記錄了代碼的運(yùn)行耗時(shí)∧畛眩現(xiàn)在重新運(yùn)行程序,結(jié)果如圖所示边翁。


async函數(shù)串行運(yùn)行耗時(shí)

????????可以看到翎承,整段代碼的運(yùn)行耗時(shí)是2032毫秒,說(shuō)明這里的兩個(gè)async函數(shù)確實(shí)是一種串行的關(guān)系符匾,前一個(gè)執(zhí)行完了后一個(gè)才能執(zhí)行叨咖。

????????但是這種寫(xiě)法明顯是非常低效的,因?yàn)閮蓚€(gè)async函數(shù)完全可以同時(shí)執(zhí)行從而提高運(yùn)行效率“〗海現(xiàn)在對(duì)上述代碼使用如下的寫(xiě)法進(jìn)行修改:

fun main() {

????runBlocking {

????????val start = System.currentTimeMillis()

????????val deferred1 = async {

????????????delay(1000)

????????????5 + 5

????????}

????????val deferred2 = async {

????????????delay(1000)

????????????4 + 6

????????}

????????println("result is ${deferred1.await() + deferred2.await()}.")

????????val end = System.currentTimeMillis()

????????println("cost ${end - start} milliseconds.")

}}

????????現(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)行程序趣倾,結(jié)果如圖所示。


async函數(shù)并行運(yùn)行耗時(shí)

????????可以看到某饰,現(xiàn)在整段代碼的運(yùn)行耗時(shí)變成了1029毫秒儒恋,運(yùn)行效率的提升顯而易見(jiàn)。

????????最后黔漂,我們?cè)賮?lái)學(xué)習(xí)一個(gè)比較特殊的作用域構(gòu)建器:withContext()函數(shù)诫尽。withContext()函數(shù)是一個(gè)掛起函數(shù),大體可以將它理解成async函數(shù)的一種簡(jiǎn)化版寫(xiě)法炬守,示例寫(xiě)法如下:

fun main() {

????runBlocking {

????????val result = withContext(Dispatchers.Default) {

????????5 + 5

????}

????println(result)

}}

????????我來(lái)解釋一下這段代碼牧嫉。調(diào)用withContext()函數(shù)之后,會(huì)立即執(zhí)行代碼塊中的代碼减途,同時(shí)將外部協(xié)程掛起酣藻。當(dāng)代碼塊中的代碼全部執(zhí)行完之后,會(huì)將最后一行的執(zhí)行結(jié)果作為withContext()函數(shù)的返回值返回观蜗,因此基本上相當(dāng)于val result = async{ 5 + 5 }.await()的寫(xiě)法臊恋。唯一不同的是,withContext()函數(shù)強(qiáng)制要求我們指定一個(gè)線程參數(shù)墓捻,關(guān)于這個(gè)參數(shù)我準(zhǔn)備好好講一講抖仅。

????????你已經(jīng)知道,協(xié)程是一種輕量級(jí)的線程的概念砖第,因此很多傳統(tǒng)編程情況下需要開(kāi)啟多線程執(zhí)行的并發(fā)任務(wù)撤卢,現(xiàn)在只需要在一個(gè)線程下開(kāi)啟多個(gè)協(xié)程來(lái)執(zhí)行就可以了。但是這并不意味著我們就永遠(yuǎn)不需要開(kāi)啟線程了梧兼,比如說(shuō)Android中要求網(wǎng)絡(luò)請(qǐng)求必須在子線程中進(jìn)行放吩,即使你開(kāi)啟了協(xié)程去執(zhí)行網(wǎng)絡(luò)請(qǐng)求,假如它是主線程當(dāng)中的協(xié)程羽杰,那么程序仍然會(huì)出錯(cuò)渡紫。這個(gè)時(shí)候我們就應(yīng)該通過(guò)線程參數(shù)給協(xié)程指定一個(gè)具體的運(yùn)行線程到推。

????????線程參數(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)目中使用驯鳖,純Kotlin程序使用這種類(lèi)型的線程參數(shù)會(huì)出現(xiàn)錯(cuò)誤闲询。

????????事實(shí)上久免,在我們剛才所學(xué)的協(xié)程作用域構(gòu)建器中浅辙,除了coroutineScope函數(shù)之外,其他所有的函數(shù)都是可以指定這樣一個(gè)線程參數(shù)的阎姥,只不過(guò)withContext()函數(shù)是強(qiáng)制要求指定的记舆,而其他函數(shù)則是可選的。

????????到目前為止呼巴,你已經(jīng)掌握了協(xié)程中最常用的一些用法泽腮,并且了解了協(xié)程的主要用途就是可以大幅度地提升并發(fā)編程的運(yùn)行效率。但實(shí)際上衣赶,Kotlin中的協(xié)程還可以對(duì)傳統(tǒng)回調(diào)的寫(xiě)法進(jìn)行優(yōu)化诊赊,從而讓代碼變得更加簡(jiǎn)潔,那么接下來(lái)我們就開(kāi)始學(xué)習(xí)這部分的內(nèi)容府瞄。

使用協(xié)程簡(jiǎn)化回調(diào)的寫(xiě)法

????????前面我們學(xué)習(xí)了編程語(yǔ)言的回調(diào)機(jī)制碧磅,并使用這個(gè)機(jī)制實(shí)現(xiàn)了獲取異步網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)響應(yīng)的功能。不知道你有沒(méi)有發(fā)現(xiàn)遵馆,回調(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)單一點(diǎn)的寫(xiě)法呢换况?

????????在過(guò)去职辨,可能確實(shí)沒(méi)有什么更加簡(jiǎn)單的寫(xiě)法了盗蟆。不過(guò)現(xiàn)在,Kotlin的協(xié)程使我們的這種設(shè)想成為了可能舒裤,只需要借助suspendCoroutine函數(shù)就能將傳統(tǒng)回調(diào)機(jī)制的寫(xiě)法大幅簡(jiǎn)化姆涩,下面我們就來(lái)具體學(xué)習(xí)一下。

????????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í)行。

????????了解了suspendCoroutine函數(shù)的作用之后黎侈,接下來(lái)我們就可以借助這個(gè)函數(shù)來(lái)對(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í)行瘤礁。接著我們?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é)果。如果請(qǐng)求成功就調(diào)用Continuation的resume()方法恢復(fù)被掛起的協(xié)程柜思,并傳入服務(wù)器響應(yīng)的數(shù)據(jù)岩调,該值會(huì)成為suspendCoroutine函數(shù)的返回值。如果請(qǐng)求失敗赡盘,就調(diào)用Continuation的resumeWithException()恢復(fù)被掛起的協(xié)程号枕,并傳入具體的異常原因。

????????你可能會(huì)說(shuō)陨享,這里不是仍然使用了傳統(tǒng)回調(diào)的寫(xiě)法嗎葱淳?代碼怎么就變得更加簡(jiǎn)化了?這是因?yàn)槊棺桑还苤笪覀円l(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)行處理

}}

????????怎么樣坑傅,有沒(méi)有覺(jué)得代碼變得清爽了很多呢?由于 getBaiduResponse()是一個(gè)掛起函數(shù)喷斋,因此當(dāng)它調(diào)用了request()函數(shù)時(shí)唁毒,當(dāng)前的協(xié)程就會(huì)被立刻掛起蒜茴,然后一直等待網(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)中诺核。

????????不過(guò)這里你可能又會(huì)產(chǎn)生新的疑惑,getBaiduResponse()函數(shù)被聲明成了掛起函數(shù)久信,這樣它也只能在協(xié)程作用域或其他掛起函數(shù)中調(diào)用了窖杀,使用起來(lái)是不是非常有局限性?確實(shí)如此裙士,因?yàn)閟uspendCoroutine函數(shù)本身就是要結(jié)合協(xié)程一起使用的入客。不過(guò)通過(guò)合理的項(xiàng)目架構(gòu)設(shè)計(jì),我們可以輕松地將各種協(xié)程的代碼應(yīng)用到一個(gè)普通的項(xiàng)目當(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)行處理

????????}})

????????有沒(méi)有覺(jué)得這里回調(diào)的寫(xiě)法也是相當(dāng)煩瑣的啃炸?不用擔(dān)心铆隘,使用suspendCoroutine函數(shù),我們馬上就能對(duì)上述寫(xiě)法進(jìn)行大幅度的簡(jiǎn)化肮帐。

????????由于不同的Service接口返回的數(shù)據(jù)類(lèi)型也不同咖驮,所以這次我們不能像剛才那樣針對(duì)具體的類(lèi)型進(jìn)行編程了,而是要使用泛型的方式训枢。定義一個(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)

????????????}

})

}}

????????這段代碼相比于剛才的request()函數(shù)又復(fù)雜了一點(diǎn)忘巧。首先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)求工育。接下來(lái)虾宇,使用同樣的方式對(duì)Retrofit響應(yīng)的數(shù)據(jù)或者網(wǎng)絡(luò)請(qǐng)求失敗的情況進(jìn)行處理就可以了。另外還有一點(diǎn)需要注意如绸,在onResponse()回調(diào)當(dāng)中嘱朽,我們調(diào)用body()方法解析出來(lái)的對(duì)象是可能為空的旭贬。如果為空的話(huà),這里的做法是手動(dòng)拋出一個(gè)異常搪泳,你也可以根據(jù)自己的邏輯進(jìn)行更加合適的處理稀轨。

????????有了await()函數(shù)之后,我們調(diào)用所有Retrofit的Service接口都會(huì)變得極其簡(jiǎn)單岸军,比如剛才同樣的功能就可以使用如下寫(xiě)法進(jìn)行實(shí)現(xià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)行處理

????????}}

????????沒(méi)有了冗長(zhǎng)的匿名類(lèi)實(shí)現(xiàn)奋刽,只需要簡(jiǎn)單調(diào)用一下await()函數(shù)就可以讓Retrofit發(fā)起網(wǎng)絡(luò)請(qǐng)求,并直接獲得服務(wù)器響應(yīng)的數(shù)據(jù)艰赞,有沒(méi)有覺(jué)得代碼變得極其簡(jiǎn)單杨名?當(dāng)然你可能會(huì)覺(jué)得,每次發(fā)起網(wǎng)絡(luò)請(qǐng)求都要進(jìn)行一次try catch處理也比較麻煩猖毫,其實(shí)這里我們也可以選擇不處理台谍。在不處理的情況下,如果發(fā)生了異常就會(huì)一層層向上拋出吁断,一直到被某一層的函數(shù)處理了為止趁蕊。因此,我們也可以在某個(gè)統(tǒng)一的入口函數(shù)中只進(jìn)行一次try catch仔役,從而讓代碼變得更加精簡(jiǎn)掷伙。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市又兵,隨后出現(xiàn)的幾起案子任柜,更是在濱河造成了極大的恐慌,老刑警劉巖沛厨,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宙地,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡逆皮,警方通過(guò)查閱死者的電腦和手機(jī)宅粥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)电谣,“玉大人秽梅,你說(shuō)我怎么就攤上這事〗宋” “怎么了企垦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)晒来。 經(jīng)常有香客問(wèn)我钞诡,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任臭增,我火速辦了婚禮懂酱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘誊抛。我一直安慰自己列牺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布拗窃。 她就那樣靜靜地躺著瞎领,像睡著了一般。 火紅的嫁衣襯著肌膚如雪九默。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天宾毒,我揣著相機(jī)與錄音驼修,去河邊找鬼。 笑死诈铛,一個(gè)胖子當(dāng)著我的面吹牛乙各,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幢竹,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼耳峦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了焕毫?” 一聲冷哼從身側(cè)響起蹲坷,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎邑飒,沒(méi)想到半個(gè)月后循签,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幸乒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年懦底,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罕扎。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丐重,靈堂內(nèi)的尸體忽然破棺而出腔召,到底是詐尸還是另有隱情,我是刑警寧澤扮惦,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布臀蛛,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏浊仆。R本人自食惡果不足惜客峭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抡柿。 院中可真熱鬧舔琅,春花似錦、人聲如沸洲劣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)囱稽。三九已至郊尝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間战惊,已是汗流浹背流昏。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吞获,地道東北人况凉。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像衫哥,于是被迫代替她去往敵國(guó)和親茎刚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容