破解 Kotlin 協(xié)程(1) - 入門篇

關(guān)鍵詞:Kotlin 協(xié)程 入門

假定你對(duì)協(xié)程(Coroutine)一點(diǎn)兒都不了解狞甚,通過閱讀本文看看是否能讓你明白協(xié)程是怎么一回事秸苗。

1. 引子

我之前寫過一些協(xié)程的文章捞镰,很久以前了础钠。那會(huì)兒還是很痛苦的怖竭,畢竟 kotlinx.coroutines 這樣強(qiáng)大的框架還在襁褓當(dāng)中锥债,于是乎我寫的幾篇協(xié)程的文章幾乎就是在告訴大家如何寫這樣一個(gè)框架——那種感覺簡(jiǎn)直糟糕透了,因?yàn)闆]有幾個(gè)人會(huì)有這樣的需求痊臭。

這次準(zhǔn)備從協(xié)程用戶(也就是程序員你我他啦)的角度來寫一下哮肚,希望對(duì)大家能有幫助。

2. 需求確認(rèn)

在開始講解協(xié)程之前广匙,我們需要先確認(rèn)幾件事兒:

  1. 你用過線程對(duì)吧允趟?
  2. 你寫過回調(diào)對(duì)吧?
  3. 你用過 RxJava 類似的框架嗎鸦致?

看下你的答案:

  • 如果上面的問題的回答都是 “Yes”潮剪,那么太好了涣楷,這篇文章非常適合你,因?yàn)槟阋呀?jīng)意識(shí)到回調(diào)有多么可怕抗碰,并且找到了解決方案狮斗;
  • 如果前兩個(gè)是 “Yes”,沒問題弧蝇,至少你已經(jīng)開始用回調(diào)了碳褒,你是協(xié)程潛在的用戶;
  • 如果只有第一個(gè)是 “Yes”看疗,那么沙峻,可能你剛剛開始學(xué)習(xí)線程,那你還是先打好基礎(chǔ)再來吧~

3. 一個(gè)常規(guī)例子

我們通過 Retrofit 發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求鹃觉,其中接口如下:

interface GitHubServiceApi {
    @GET("users/{login}")
    fun getUser(@Path("login") login: String): Call<User>
}

data class User(val id: String, val name: String, val url: String)

Retrofit 初始化如下:

val gitHubServiceApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder()
            .baseUrl("https://api.github.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    retrofit.create(GitHubServiceApi::class.java)
}

那么我們請(qǐng)求網(wǎng)絡(luò)時(shí):

gitHubServiceApi.getUser("bennyhuo").enqueue(object : Callback<User> {
    override fun onFailure(call: Call<User>, t: Throwable) {
        handler.post { showError(t) }
    }

    override fun onResponse(call: Call<User>, response: Response<User>) {
        handler.post { response.body()?.let(::showUser) ?: showError(NullPointerException()) }
    }
})

請(qǐng)求結(jié)果回來之后专酗,我們切換線程到 UI 線程來展示結(jié)果。這類代碼大量存在于我們的邏輯當(dāng)中盗扇,它有什么問題呢?

  • 通過 Lambda 表達(dá)式沉填,我們讓線程切換變得不是那么明顯疗隶,但它仍然存在,一旦開發(fā)者出現(xiàn)遺漏翼闹,這里就會(huì)出現(xiàn)問題
  • 回調(diào)嵌套了兩層斑鼻,看上去倒也沒什么,但真實(shí)的開發(fā)環(huán)境中邏輯一定比這個(gè)復(fù)雜的多猎荠,例如登錄失敗的重試
  • 重復(fù)或者分散的異常處理邏輯坚弱,在請(qǐng)求失敗時(shí)我們調(diào)用了一次 showError,在數(shù)據(jù)讀取失敗時(shí)我們又調(diào)用了一次关摇,真實(shí)的開發(fā)環(huán)境中可能會(huì)有更多的重復(fù)

Kotlin 本身的語法已經(jīng)讓這段代碼看上去好很多了荒叶,如果用 Java 寫的話,你的直覺都會(huì)告訴你:你在寫 Bug输虱。

如果你不是 Android 開發(fā)者些楣,那么你可能不知道 handler 是什么東西,沒關(guān)系宪睹,你可以替換為 SwingUtilities.invokeLater{ ... } (Java Swing)愁茁,或者 setTimeout({ ... }, 0) (Js) 等等。

4. 改造成協(xié)程

你當(dāng)然可以改造成 RxJava 的風(fēng)格亭病,但 RxJava 比協(xié)程抽象多了鹅很,因?yàn)槌悄闶炀毷褂媚切?operator,不然你根本不知道它在干嘛(試想一下 retryWhen)罪帖。協(xié)程就不一樣了促煮,畢竟編譯器加持邮屁,它可以很簡(jiǎn)潔的表達(dá)出代碼的邏輯,不要想它背后的實(shí)現(xiàn)邏輯污茵,它的運(yùn)行結(jié)果就是你直覺告訴你的那樣樱报。

對(duì)于 Retrofit,改造成協(xié)程的寫法泞当,有兩種迹蛤,分別是通過 CallAdapter 和 suspend 函數(shù)。

4.1 CallAdapter 的方式

我們先來看看 CallAdapter 的方式襟士,這個(gè)方式的本質(zhì)是讓接口的方法返回一個(gè)協(xié)程的 Job:

interface GitHubServiceApi {
    @GET("users/{login}")
    fun getUser(@Path("login") login: String): Deferred<User>
}

注意 Deferred 是 Job 的子接口盗飒。

那么我們需要為 Retrofit 添加對(duì) Deferred 的支持,這需要用到開源庫:

implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'

構(gòu)造 Retrofit 實(shí)例時(shí)添加:

val gitHubServiceApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder()
            .baseUrl("https://api.github.com")
            .addConverterFactory(GsonConverterFactory.create())
            //添加對(duì) Deferred 的支持
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()

    retrofit.create(GitHubServiceApi::class.java)
}

那么這時(shí)候我們發(fā)起請(qǐng)求就可以這么寫了:

GlobalScope.launch(Dispatchers.Main) {
    try {
        showUser(gitHubServiceApi.getUser("bennyhuo").await())
    } catch (e: Exception) {
        showError(e)
    }
}

說明: Dispatchers.Main 在不同的平臺(tái)上的實(shí)現(xiàn)不同陋桂,如果在 Android 上為 HandlerDispatcher逆趣,在 Java Swing 上為 SwingDispatcher 等等。

首先我們通過 launch 啟動(dòng)了一個(gè)協(xié)程嗜历,這類似于我們啟動(dòng)一個(gè)線程宣渗,launch 的參數(shù)有三個(gè),依次為協(xié)程上下文梨州、協(xié)程啟動(dòng)模式痕囱、協(xié)程體:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext, // 上下文
    start: CoroutineStart = CoroutineStart.DEFAULT,  // 啟動(dòng)模式
    block: suspend CoroutineScope.() -> Unit // 協(xié)程體
): Job 

啟動(dòng)模式不是一個(gè)很復(fù)雜的概念,不過我們暫且不管暴匠,默認(rèn)直接允許調(diào)度執(zhí)行鞍恢。

上下文可以有很多作用,包括攜帶參數(shù)每窖,攔截協(xié)程執(zhí)行等等帮掉,多數(shù)情況下我們不需要自己去實(shí)現(xiàn)上下文,只需要使用現(xiàn)成的就好窒典。上下文有一個(gè)重要的作用就是線程切換蟆炊,Dispatchers.Main 就是一個(gè)官方提供的上下文,它可以確保 launch 啟動(dòng)的協(xié)程體運(yùn)行在 UI 線程當(dāng)中(除非你自己在 launch 的協(xié)程體內(nèi)部進(jìn)行線程切換崇败、或者啟動(dòng)運(yùn)行在其他有線程切換能力的上下文的協(xié)程)盅称。

換句話說,在例子當(dāng)中整個(gè) launch 內(nèi)部你看到的代碼都是運(yùn)行在 UI 線程的后室,盡管 getUser 在執(zhí)行的時(shí)候確實(shí)切換了線程缩膝,但返回結(jié)果的時(shí)候會(huì)再次切回來。這看上去有些費(fèi)解岸霹,因?yàn)橹庇X告訴我們疾层,getUser 返回了一個(gè) Deferred 類型,它的 await 方法會(huì)返回一個(gè) User 對(duì)象贡避,意味著 await 需要等待請(qǐng)求結(jié)果返回才可以繼續(xù)執(zhí)行痛黎,那么 await 不會(huì)阻塞 UI 線程嗎予弧?

答案是:不會(huì)。當(dāng)然不會(huì)湖饱,不然那 DeferredFuture 又有什么區(qū)別呢掖蛤?這里 await 就很可疑了,因?yàn)樗鼘?shí)際上是一個(gè) suspend 函數(shù)井厌,這個(gè)函數(shù)只能在協(xié)程體或者其他 suspend 函數(shù)內(nèi)部被調(diào)用蚓庭,它就像是回調(diào)的語法糖一樣,它通過一個(gè)叫 Continuation 的接口的實(shí)例來返回結(jié)果:

@SinceKotlin("1.3")
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

1.3 的源碼其實(shí)并不是很直接仅仆,盡管我們可以再看下 Result 的源碼器赞,但我不想這么做刨仑。更容易理解的是之前版本的源碼:

@SinceKotlin("1.1")
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resume(value: T)
    public fun resumeWithException(exception: Throwable)
}

相信大家一下就能明白匿沛,這其實(shí)就是個(gè)回調(diào)嘛。如果還不明白胳赌,那就對(duì)比下 Retrofit 的 Callback

public interface Callback<T> {
  void onResponse(Call<T> call, Response<T> response);
  void onFailure(Call<T> call, Throwable t);
}

有結(jié)果正常返回的時(shí)候咳榜,Continuation 調(diào)用 resume 返回結(jié)果夏醉,否則調(diào)用 resumeWithException 來拋出異常,簡(jiǎn)直與 Callback 一模一樣涌韩。

所以這時(shí)候你應(yīng)該明白授舟,這段代碼的執(zhí)行流程本質(zhì)上是一個(gè)異步回調(diào):

GlobalScope.launch(Dispatchers.Main) {
    try {
        //showUser 在 await 的 Continuation 的回調(diào)函數(shù)調(diào)用后執(zhí)行
        showUser(gitHubServiceApi.getUser("bennyhuo").await())
    } catch (e: Exception) {
        showError(e)
    }
}

而代碼之所以可以看起來是同步的,那就是編譯器的黑魔法了贸辈,你當(dāng)然也可以叫它“語法糖”。

這時(shí)候也許大家還是有問題:我并沒有看到 Continuation 啊肠槽,沒錯(cuò)擎淤,這正是我們前面說的編譯器黑魔法了,在 Java 虛擬機(jī)上秸仙,await 這個(gè)方法的簽名其實(shí)并不像我們看到的那樣:

public suspend fun await(): T

它真實(shí)的簽名其實(shí)是:

kotlinx/coroutines/Deferred.await (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

即接收一個(gè) Continuation 實(shí)例嘴拢,返回 Object 的這么個(gè)函數(shù),所以前面的代碼我們可以大致理解為:

//注意以下不是正確的代碼寂纪,僅供大家理解協(xié)程使用
GlobalScope.launch(Dispatchers.Main) {
    gitHubServiceApi.getUser("bennyhuo").await(object: Continuation<User>{
            override fun resume(value: User) {
                showUser(value)
            }
            override fun resumeWithException(exception: Throwable){
                showError(exception)
            }
    })
}

而在 await 當(dāng)中席吴,大致就是:

//注意以下并不是真實(shí)的實(shí)現(xiàn),僅供大家理解協(xié)程使用
fun await(continuation: Continuation<User>): Any {
    ... // 切到非 UI 線程中執(zhí)行捞蛋,等待結(jié)果返回
    try {
        val user = ...
        handler.post{ continuation.resume(user) }
    } catch(e: Exception) {
        handler.post{ continuation.resumeWithException(e) }
    }
}

這樣的回調(diào)大家一看就能明白孝冒。講了這么多,請(qǐng)大家記住一點(diǎn):從執(zhí)行機(jī)制上來講拟杉,協(xié)程跟回調(diào)沒有什么本質(zhì)的區(qū)別庄涡。

4.2 suspend 函數(shù)的方式

suspend 函數(shù)是 Kotlin 編譯器對(duì)協(xié)程支持的唯一的黑魔法(表面上的,還有其他的我們后面講原理的時(shí)候再說)了搬设,我們前面已經(jīng)通過 Deferredawait 方法對(duì)它有了個(gè)大概的了解穴店,我們?cè)賮砜纯?Retrofit 當(dāng)中它還可以怎么用撕捍。

Retrofit 當(dāng)前的 release 版本是 2.5.0,還不支持 suspend 函數(shù)泣洞。因此想要嘗試下面的代碼忧风,需要最新的 Retrofit 源碼的支持;當(dāng)然球凰,也許你看到這篇文章的時(shí)候狮腿,Retrofit 的新版本已經(jīng)支持這一項(xiàng)特性了呢。

首先我們修改接口方法:

@GET("users/{login}")
suspend fun getUser(@Path("login") login: String): User

這種情況 Retrofit 會(huì)根據(jù)接口方法的聲明來構(gòu)造 Continuation弟蚀,并且在內(nèi)部封裝了 Call 的異步請(qǐng)求(使用 enqueue)蚤霞,進(jìn)而得到 User 實(shí)例,具體原理后面我們有機(jī)會(huì)再介紹义钉。使用方法如下:

GlobalScope.launch {
    try {
        showUser(gitHubServiceApi.getUser("bennyhuo"))
    } catch (e: Exception) {
        showError(e)
    }
}

它的執(zhí)行流程與 Deferred.await 類似昧绣,我們就不再詳細(xì)分析了。

5. 協(xié)程到底是什么

好捶闸,堅(jiān)持讀到這里的朋友們夜畴,你們一定是異步代碼的“受害者”,你們肯定遇到過“回調(diào)地獄”删壮,它讓你的代碼可讀性急劇降低贪绘;也寫過大量復(fù)雜的異步邏輯處理、異常處理央碟,這讓你的代碼重復(fù)邏輯增加税灌;因?yàn)榛卣{(diào)的存在,還得經(jīng)常處理線程切換亿虽,這似乎并不是一件難事菱涤,但隨著代碼體量的增加,它會(huì)讓你抓狂洛勉,線上上報(bào)的異常因線程使用不當(dāng)導(dǎo)致的可不在少數(shù)粘秆。

協(xié)程可以幫你優(yōu)雅的處理掉這些。

協(xié)程本身是一個(gè)脫離語言實(shí)現(xiàn)的概念收毫,我們“很嚴(yán)謹(jǐn)”(哈哈)的給出維基百科的定義:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

簡(jiǎn)單來說就是攻走,協(xié)程是一種非搶占式或者說協(xié)作式的計(jì)算機(jī)程序并發(fā)調(diào)度的實(shí)現(xiàn),程序可以主動(dòng)掛起或者恢復(fù)執(zhí)行此再。這里還是需要有點(diǎn)兒操作系統(tǒng)的知識(shí)的昔搂,我們?cè)?Java 虛擬機(jī)上所認(rèn)識(shí)到的線程大多數(shù)的實(shí)現(xiàn)是映射到內(nèi)核的線程的,也就是說線程當(dāng)中的代碼邏輯在線程搶到 CPU 的時(shí)間片的時(shí)候才可以執(zhí)行引润,否則就得歇著巩趁,當(dāng)然這對(duì)于我們開發(fā)者來說是透明的;而經(jīng)常聽到所謂的協(xié)程更輕量的意思是,協(xié)程并不會(huì)映射成內(nèi)核線程或者其他這么重的資源议慰,它的調(diào)度在用戶態(tài)就可以搞定蠢古,任務(wù)之間的調(diào)度并非搶占式,而是協(xié)作式的别凹。

關(guān)于并發(fā)和并行:正因?yàn)?CPU 時(shí)間片足夠小草讶,因此即便一個(gè)單核的 CPU,也可以給我們營(yíng)造多任務(wù)同時(shí)運(yùn)行的假象炉菲,這就是所謂的“并發(fā)”堕战。并行才是真正的同時(shí)運(yùn)行。并發(fā)的話拍霜,更像是 Magic嘱丢。

如果大家熟悉 Java 虛擬機(jī)的話,就想象一下 Thread 這個(gè)類到底是什么吧祠饺,為什么它的 run 方法會(huì)運(yùn)行在另一個(gè)線程當(dāng)中呢越驻?誰負(fù)責(zé)執(zhí)行這段代碼的呢?顯然道偷,咋一看缀旁,Thread 其實(shí)是一個(gè)對(duì)象而已,run 方法里面包含了要執(zhí)行的代碼——僅此而已勺鸦。協(xié)程也是如此并巍,如果你只是看標(biāo)準(zhǔn)庫的 API,那么就太抽象了换途,但我們開篇交代了懊渡,學(xué)習(xí)協(xié)程不要上來去接觸標(biāo)準(zhǔn)庫,kotlinx.coroutines 框架才是我們用戶應(yīng)該關(guān)心的军拟,而這個(gè)框架里面對(duì)應(yīng)于 Thread 的概念就是 Job 了距贷,大家可以看下它的定義:

public interface Job : CoroutineContext.Element {
    ...
    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean

    public fun start(): Boolean
    public fun cancel(cause: CancellationException? = null)
    public suspend fun join()
    ...
}

我們?cè)賮砜纯?Thread 的定義:

public class Thread implements Runnable {
    ...    
    public final native boolean isAlive();
    public synchronized void start() { ... }
    @Deprecated
    public final void stop() { ... }
    public final void join() throws InterruptedException  { ... }
    ...
}

這里我們非常貼心的省略了一些注釋和不太相關(guān)的接口。我們發(fā)現(xiàn)吻谋,Thread 與 Job 基本上功能一致,它們都承載了一段代碼邏輯(前者通過 run 方法现横,后者通過構(gòu)造協(xié)程用到的 Lambda 或者函數(shù))漓拾,也都包含了這段代碼的運(yùn)行狀態(tài)。

而真正調(diào)度時(shí)二者才有了本質(zhì)的差異戒祠,具體怎么調(diào)度骇两,我們只需要知道調(diào)度結(jié)果就能很好的使用它們了。

6. 小結(jié)

我們先通過例子來引入姜盈,從大家最熟悉的代碼到協(xié)程的例子開始低千,演化到協(xié)程的寫法,讓大家首先能從感性上對(duì)協(xié)程有個(gè)認(rèn)識(shí),最后我們給出了協(xié)程的定義示血,也告訴大家協(xié)程究竟能做什么棋傍。

這篇文章沒有追求什么內(nèi)部原理,只是企圖讓大家對(duì)協(xié)程怎么用有個(gè)第一印象难审。如果大家仍然感覺到迷惑瘫拣,不怕,后面我將再用幾篇文章從例子入手來帶著大家分析協(xié)程的運(yùn)行告喊,而原理的分析麸拄,會(huì)放到大家能夠熟練掌握協(xié)程之后再來探討。


歡迎關(guān)注 Kotlin 中文社區(qū)黔姜!

中文官網(wǎng):https://www.kotlincn.net/

中文官方博客:https://www.kotliner.cn/

公眾號(hào):Kotlin

知乎專欄:Kotlin

CSDN:Kotlin中文社區(qū)

掘金:Kotlin中文社區(qū)

簡(jiǎn)書:Kotlin中文社區(qū)

開發(fā)者頭條:Kotlin中文社區(qū)

?著作權(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)離奇詭異实苞,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)烈疚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爷肝,“玉大人,你說我怎么就攤上這事灯抛。” “怎么了对嚼?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)纵竖。 經(jīng)常有香客問我,道長(zhǎng)靡砌,這世上最難降的妖魔是什么已脓? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮通殃,結(jié)果婚禮上度液,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好堕担,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布已慢。 她就那樣靜靜地躺著,像睡著了一般照宝。 火紅的嫁衣襯著肌膚如雪蛇受。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天厕鹃,我揣著相機(jī)與錄音兢仰,去河邊找鬼。 笑死剂碴,一個(gè)胖子當(dāng)著我的面吹牛把将,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忆矛,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼察蹲,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了催训?” 一聲冷哼從身側(cè)響起洽议,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎漫拭,沒想到半個(gè)月后亚兄,有當(dāng)?shù)厝嗽跇淞掷锇l(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
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望跟磨。 院中可真熱鬧攒盈,春花似錦、人聲如沸僵蛛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驼侠。三九已至谆吴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笋熬,已是汗流浹背突诬。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工芜繁, 沒想到剛下飛機(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)容