Kotlin 協(xié)程之取消與異常處理探索之旅(下)

前言

協(xié)程系列文章:

上篇分析了線程異常&取消操作以及協(xié)程Job相關知識厘唾,有了這些基礎知識,我們再來看協(xié)程的取消與異常處理就比較簡單了鹤树。
通過本篇文章罕伯,你將了解到:

  1. 協(xié)程取消的幾種方式
  2. 協(xié)程異常處理幾種方式
  3. 協(xié)程異常傳遞原理

1. 協(xié)程取消的幾種方式

非阻塞狀態(tài)時取消

先看Demo:

class CancelDemo {
    fun testCancel() {
        runBlocking() {
            var job1 = launch(Dispatchers.IO) {
                println("job1 start")
                Thread.sleep(200)
                var count = 0
                while (count < 1000000000) {
                    count++
                }
                println("job1 end count:$count")
            }
            Thread.sleep(100)
            println("start cancel job1")
            //取消job(取消協(xié)程)
            job1.cancel()
            println("end cancel job1")
        }
    }
}

fun main(args: Array<String>) {
    var demo = CancelDemo()
    demo.testCancel()
    Thread.sleep(1000000)
}

先啟動一個子協(xié)程坟募,它返回Job對象,當子協(xié)程成功運行后再取消它赚哗。
結果如下:


image.png

該打印反饋出兩個信息:

  1. 子協(xié)程啟動并運行后才開始取消它。
  2. 子協(xié)程并沒有終止運行扩所,而是正常運行到結束祖屏。

你可能對第2點比較困惑,為啥取消沒效果呢期丰?
還記得我們上篇分析的線程的終止嗎钝荡?在非阻塞狀態(tài)下埠通,通過Thread.interrupt()調用下僅僅只是喚醒線程并且設置標記位。
與線程類似,協(xié)程Job.cancel()函數(shù)僅僅只是將state值改變而已渗柿,當然我們可以主動獲取協(xié)程當前的狀態(tài)做祝。

        runBlocking() {
            var job1 = launch(Dispatchers.IO) {
                println("job1 start")
                Thread.sleep(80)
                var count = 0
                //判斷協(xié)程的狀態(tài),若是活躍則繼續(xù)循環(huán)
                //isActive = coroutineContext[Job]?.isActive ?: true
                while (count < 1000000000 && isActive) {
                    count++
                }
                println("job1 end count:$count")
            }
            Thread.sleep(100)
            println("start cancel job1")
            //取消job(取消協(xié)程)
            job1.cancel()
            println("end cancel job1")
        }
    }

運行結果:


image.png

從打印結果可以看出:

協(xié)程確實被取消了,可以通過Job.isActive 判斷取消是否成功悯嗓,若Job.isActive = false 則表示協(xié)程被取消了。

阻塞狀態(tài)時取消

說到阻塞狀態(tài)合武,你可能會說:"簡單稼跳,我?guī)仔写a就給你演示了:"

    fun testCancel3() {
        runBlocking() {
            var job1 = launch(Dispatchers.IO) {
                Thread.sleep(3000)
                println("coroutine isActive:$isActive")//①
            }
            Thread.sleep(100)
            println("start cancel job1")
            //取消job(取消協(xié)程)
            job1.cancel()
            println("end cancel job1")
        }
    }

先猜猜①會打印嗎?有同學說不會打印红淡,因為Thread.sleep(xx)方法會拋出異常。
實際結果卻是:①會打印颈渊。
認為不會打印的同學可能將線程的阻塞與協(xié)程的阻塞(掛起)混淆了,Thread.sleep(xx)是阻塞協(xié)程所在的線程终佛,它是線程的專屬方法俊嗽,因此它會響應線程的中斷:Thread.interrupt()并拋出異常,而不會響應協(xié)程的Job.cancel()函數(shù)铃彰。
協(xié)程阻塞(掛起)并不會阻塞其所在的線程绍豁,改造Demo如下:

    fun testCancel4() {
        runBlocking() {
            var job1 = launch(Dispatchers.IO) {
                //協(xié)程掛起
                println("job1 start")
                delay(3000)
                println("coroutine isActive:$isActive")//①
            }
            Thread.sleep(100)
            println("start cancel job1")
            //取消job(取消協(xié)程)
            job1.cancel()
            println("end cancel job1")
        }
    }

觀察打印結果,我們發(fā)現(xiàn)①始終無法打印出來牙捉,我們有理由相信協(xié)程執(zhí)行到delay(xx)時拋出了異常竹揍,導致后續(xù)的代碼無法執(zhí)行昧碉,接著驗證猜想闪金。

    fun testCancel4() {
        runBlocking() {
            var job1 = launch(Dispatchers.IO) {
                //協(xié)程掛起
                println("job1 start")
                try {
                    delay(3000)
                } catch (e : Exception) {
                    println("delay exception:$e")
                }
                println("coroutine isActive:$isActive")//①
            }
            Thread.sleep(100)
            println("start cancel job1")
            //取消job(取消協(xié)程)
            job1.cancel()
            println("end cancel job1")
        }
    }

如上,給delay(xx)函數(shù)加了異常處理蛙奖,打印結果如下:

image.png

果然不出所料缸兔,Job.cancel(xx)引發(fā)了delay(xx)異常,它拋出的異常為:JobCancellationException,該異常在JVM平臺繼承自CancellationException驮履。

如何"優(yōu)雅"地取消協(xié)程

結合阻塞/非阻塞狀態(tài)下取消協(xié)程的分析恐似,與線程處理方式類似:對于阻塞狀態(tài)的協(xié)程双藕,我們可以捕獲異常,對于非阻塞的地方我們使用狀態(tài)判斷斥杜。
根據(jù)不同的結果來決定協(xié)程被取消后代碼的處理邏輯。

    fun testCancel5() {
        runBlocking() {
            var job1 = launch(Dispatchers.IO) {
                try {
                    //掛起函數(shù)
                } catch (e : Exception) {
                    println("delay exception:$e")
                }
                if (!isActive) {
                    println("cancel")
                }
            }
        }
    }

2. 協(xié)程異常處理幾種方式

try...catch異常

上面提及了協(xié)程的取消異常,它是比較特殊的異常犁罩,我們先來看看普通的異常處理。

    fun testException() {
        runBlocking {
            try {
                var job1 = launch(Dispatchers.IO) {
                    println("job1 start")
                    //異常
                    1 / 0
                    println("job1 end")
                }
            } catch (e: Exception) {
            }
        }
    }

先猜猜這樣能夠捕獲異常嗎缎脾?根據(jù)我們上篇線程異常捕獲的經(jīng)驗,此處的子協(xié)程運行在子線程里域滥,在子線程里發(fā)生的異常腊嗡,主線程當然無法通過try 捕獲到。
當然建邓,萬能的方式是在子協(xié)程里捕獲:

    fun testException2() {
        runBlocking {
            var job1 = launch(Dispatchers.IO) {
                try {
                    println("job1 start")
                    //異常
                    1 / 0
                    println("job1 end")
                } catch (e : Exception) {
                    println("e=$e")
                }
            }
        }
    }

全局捕獲異常

與線程類似塔嬉,協(xié)程也可以全局捕獲異常。

    //創(chuàng)建處理異常對象
    val exceptionHandler = CoroutineExceptionHandler { _, exception ->
        println("handle exception:$exception")
    }
    fun testException3() {
        runBlocking {
            //聲明協(xié)程作用域
            var scope = CoroutineScope(Job() + exceptionHandler)
            var job1 = scope.launch(Dispatchers.IO) {
                println("job1 start")
                //異常
                1 / 0
                println("job1 end")
            }
        }
    }

如上Demo勾邦,先定義一個異常處理對象,然后將它與協(xié)程作用域關聯(lián)起來休弃。
當子協(xié)程發(fā)生了異常顿仇,這個異常往上拋給父Job黎泣,最后交給CoroutineExceptionHandler 處理。

image.png

此時交掏,ArithmeticException 異常被CoroutineExceptionHandler 捕獲了。
注,雖然能夠捕獲異常吨娜,但是發(fā)生異常的協(xié)程還是不能往下執(zhí)行了燎斩。

3. 協(xié)程異常傳遞原理

協(xié)程對異常的再加工

launch{}

花括號里的內容即為協(xié)程體敛滋,而執(zhí)行這部分的邏輯在BaseContinuationImpl.resumeWith()函數(shù)里:


image.png

你可發(fā)現(xiàn)此處的重點擎勘?
這里將協(xié)程體的執(zhí)行加了try...catch 捕獲了,也就是說不論協(xié)程體里發(fā)生了什么異常,在這里都能夠被捕獲沼填。
你可能會問了,既然能夠捕獲螟蝙,為啥還會有異常拋出呢取具?我們有理由相信,協(xié)程內部一定記錄了這個異常扁耐,然后在某個地方再次將它拋出暇检。
此處捕獲了異常之后,將它構造為Result婉称,并記錄在變量outcome里块仆,接著看看后續(xù)對這個值的處理。
流程有點長王暗,直接看調用棧:


image.png

重點看紅色框里的兩個函數(shù)悔据。

#handleCoroutineExceptionImpl.kt
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
    try {
        //從context里取出異常處理對象探越,對應外部設置的全局捕獲回調對象
        context[CoroutineExceptionHandler]?.let {
            //具體處理
            it.handleException(context, exception)
            //處理ok募判,直接退出
            return
        }
    } catch (t: Throwable) {
        handleCoroutineExceptionImpl(context, handlerException(exception, t))
        return
    }
    //再次嘗試處理
    handleCoroutineExceptionImpl(context, exception)
}

internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
    // 嘗試handler處理
    // 從當前線程拋出異常
    val currentThread = Thread.currentThread()
    currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}

如果我們定義了CoroutineExceptionHandler麻养,那么使用該Handler處理異常想帅,如果沒有定義哨颂,則直接拋出異常紊遵。
以上即為協(xié)程對異常的再加工處理過程嗅回。

異常在協(xié)程之間的傳遞(Job)

先看Demo:

    fun testException4() {
        runBlocking {
            //聲明協(xié)程作用域
            var rootJob = Job()
            var scope = CoroutineScope(rootJob)
            var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {
                println("job1 start")
                //異常
                1 / 0
                println("job1 end")
            }

            job1.join()
            //檢查父Job 狀態(tài)
            println("rootJob isActive:${rootJob.isActive}")
        }
    }

rootJob 作為父Job洞坑,通過launch(xx)函數(shù)創(chuàng)建了子Job:job1之众。
等待job1執(zhí)行完畢后拙毫,再檢查父Job 狀態(tài)。
打印結果如下:


image.png

此時我們發(fā)現(xiàn):

當子Job 發(fā)生異常時棺禾,會取消父Job缀蹄。

除了對父Job 有影響,對其它兄弟Job 是否有影響呢膘婶?
繼續(xù)做嘗試:

    fun testException5() {
        runBlocking {
            //聲明協(xié)程作用域
            var rootJob = Job()
            var scope = CoroutineScope(rootJob)
            var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {
                println("job1 start")
                Thread.sleep(100)
                //異常
                1 / 0
                println("job1 end")
            }

            var job2 = scope.launch {
                println("job2 start")
                Thread.sleep(200)
                //檢查jo2狀態(tài)
                println("jo2 isActive:$isActive")
            }

            job1.join()
            //檢查父Job 狀態(tài)
            println("rootJob isActive:${rootJob.isActive}")
        }
    }

如上缺前,父Job 分別創(chuàng)建了兩個子Job:job1、job2悬襟,當job1 發(fā)生異常時衅码,分別檢測父Job與job2的狀態(tài),打印結果如下:


image.png

很明顯得出結論:

當子Job 發(fā)生異常時脊岳,會將異常傳遞給父Job逝段,父Job 先將自己名下的所有子Job都取消,然后將自己取消割捅,最后繼續(xù)將異常往上拋奶躯。

這部分的傳遞依靠Job 鏈完成,上篇文章我們有深入分析過Job 結構:


image.png

從源碼分析其傳遞流程亿驾,先看調用棧:


image.png

重點看notifyCancelling(xx)函數(shù):

#JobSupport.kt
//list == 子Job 鏈表
private fun notifyCancelling(list: NodeList, cause: Throwable) {
    //回調嘹黔,忽略
    onCancelling(cause)
    //取消所有子Job
    notifyHandlers<JobCancellingNode>(list, cause)//①
    //取消父Job
    cancelParent(cause) //②
}

分為兩個要點:

#JobSupport.kt
private inline fun <reified T: JobNode> notifyHandlers(list: NodeList, cause: Throwable?) {
    var exception: Throwable? = null
    list.forEach<T> { node ->
        try {
            //遍歷list,調用node
            node.invoke(cause)
        } catch (ex: Throwable) {
            //...
        }
    }
    //..
}

調用至此莫瞬,實際上是job1.notifyCancelling(xx)儡蔓,因為job1沒有子Job郭蕉,因此①處list 里沒有節(jié)點。

#JobSupport.kt
private fun cancelParent(cause: Throwable): Boolean {
    val isCancellation = cause is CancellationException
    val parent = parentHandle
    if (parent === null || parent === NonDisposableHandle) {
        //沒有父Job喂江,無法繼續(xù)往上召锈,停止
        return isCancellation
    }
    //取消父Job
    return parent.childCancelled(cause) || isCancellation
}

如果你看過上篇文章的分析,再看此處就比較容易了开呐,此處再貼一下Node 結構:

#JobSupport.kt
//主要有2個成員變量
//childJob: ChildJob 表示當前node指向的子Job
//parent: Job 表示當前node 指向的父Job
internal class ChildHandleNode(
    @JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
    override val parent: Job get() = job
    //父Job 取消其所有子Job
    override fun invoke(cause: Throwable?) = childJob.parentCancelled(job)
    //子Job向上傳遞烟勋,取消父Job
    override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}

對于①來說,list 里的node 為ChildHandleNode筐付,node.invoke(cause)其實調用的就是childJob.parentCancelled(job)卵惦,而childJob 表示每個子Job。

    #JobSupport.kt
    public final override fun parentCancelled(parentJob: ParentJob) {
        //遍歷Job 下的子Job瓦戚,取消它們
        cancelImpl(parentJob)
    }

就這么層層遍歷下去沮尿,直至取消完所有層級的子Job。

而對于②而言较解,parent.childCancelled(cause)==job.childCancelled(cause)畜疾,而job 表示的是當前job 的父Job。

    #JobSupport.kt
    public open fun childCancelled(cause: Throwable): Boolean {
        //如果是取消異常印衔,則忽略
        if (cause is CancellationException) return true
        //取消父Job
        return cancelImpl(cause) && handlesException
    }

這段代碼透露出兩個意思:

  1. 取消時候產(chǎn)生的異常稱為"取消異常"啡捶,該異常比較特殊,當某個job 發(fā)生異常時奸焙,它不會往上傳遞瞎暑。
  2. 如果不是取消異常,則調用cancelImpl(xx)函數(shù)与帆,該函數(shù)取消當前Job的所有子Job 與自己了赌。

因為Job 鏈類似樹的結構,因此異常傳遞是遞歸形式的玄糟。

Job 發(fā)生異常時勿她,不僅取消自己名下的所有Job,也會取消父Job阵翎,往上遞歸直至根Job逢并。

SupervisorJob 作用與原理

作用

子協(xié)程發(fā)生異常后,會取消父協(xié)程郭卫、兄弟協(xié)程的執(zhí)行筒狠,這在有些場景是不合理的,因為傷害范圍太廣箱沦,明明是一個子協(xié)程的鍋,非得所有協(xié)程來背雇庙。
還好官方考慮過這個問題谓形,提供了SupervisorJob 來解決該問題灶伊。

    fun testException6() {
        runBlocking {
            //聲明協(xié)程作用域
            var rootJob = SupervisorJob()
            var scope = CoroutineScope(rootJob)
            var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {
                println("job1 start")
                Thread.sleep(100)
                //異常
                1 / 0
                println("job1 end")
            }
            var job2 = scope.launch {
                println("job2 start")
                Thread.sleep(200)
                //檢查jo2狀態(tài)
                println("jo2 isActive:$isActive")
            }

            job1.join()
            //檢查父Job 狀態(tài)
            println("rootJob isActive:${rootJob.isActive}")
        }
    }

僅僅改動了一個地方:將Job()換為SupervisorJob()。
結果如下:


image.png

job1 發(fā)生異常的時候寒跳,job2 和父job都沒受到影響聘萨。

原理
當需要取消父Job 時,勢必會調用到:job.childCancelled(cause)
而SupervisorJob 重寫了該函數(shù):

#Supervisor.kt
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

不做任何處理童太,當然就不能取消父Job了米辐,不能取消父Job,也就不能取消父Job 下的子Job书释。

對比Job()與SupervisorJob() 可知:


image.png

取消異常的傳遞

job.childCancelled(cause) 表示要取消父Job翘贮,而該函數(shù)實現(xiàn)里有對取消異常進行了特殊處理,因此取消異常不會往上傳遞爆惧。

    fun testException7() {
        runBlocking {
            //聲明協(xié)程作用域
            var rootJob = SupervisorJob()
            var scope = CoroutineScope(rootJob)
            var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {
                println("job1 start")
                Thread.sleep(2000)
                println("job1 end")
            }

            var job2 = scope.launch {
                println("job2 start")
                Thread.sleep(1000)
                //檢查jo2狀態(tài)
                println("jo2 isActive:$isActive")
            }

            Thread.sleep(300)
            job1.cancel()
            //檢查父Job 狀態(tài)
            println("rootJob isActive:${rootJob.isActive}")
        }
    }

取消job1狸页,不會影響父Job,也不會影響子Job扯再。

當取消父Job時芍耘,查看子Job 是否受影響。

    fun testException8() {
        runBlocking {
            //聲明協(xié)程作用域
            var rootJob = SupervisorJob()
            var scope = CoroutineScope(rootJob)
            var job1 = scope.launch(Dispatchers.IO + exceptionHandler) {
                println("job1 start")
                Thread.sleep(2000)
                println("jo1 isActive:$isActive")
            }

            var job2 = scope.launch {
                println("job2 start")
                Thread.sleep(1000)
                //檢查jo2狀態(tài)
                println("jo2 isActive:$isActive")
            }

            Thread.sleep(300)
            rootJob.cancel()
            //檢查父Job 狀態(tài)
            println("rootJob isActive:${rootJob.isActive}")
        }
    }

當父Job 取消時熄阻,子Job 都會被取消斋竞。

至此,所有內容分析完畢秃殉,小結一下之前的內容:

  1. 協(xié)程的異常會沿著Job鏈傳遞坝初,子協(xié)程發(fā)生異常會導致父協(xié)程(祖父協(xié)程...)、兄弟協(xié)程的取消复濒。
  2. 若要防止上述情況脖卖,需要使用SupervisorJob作為父Job,它將忽略子Job產(chǎn)生的異常巧颈,不將它傳遞出去畦木。
  3. 取消異常不會向上傳遞,父協(xié)程的取消會導致其下所有的子協(xié)程被取消砸泛。

關于協(xié)程的取消與異常處理到此分析完畢十籍,下篇將分析launch/async/delay/runBlocking 的使用、原理以及異同點唇礁。

本文基于Kotlin 1.5.3勾栗,文中完整Demo請點擊

您若喜歡,請點贊盏筐、關注围俘、收藏,您的鼓勵是我前進的動力

持續(xù)更新中,和我一起步步為營系統(tǒng)界牡、深入學習Android/Kotlin

1簿寂、Android各種Context的前世今生
2、Android DecorView 必知必會
3宿亡、Window/WindowManager 不可不知之事
4常遂、View Measure/Layout/Draw 真明白了
5、Android事件分發(fā)全套服務
6挽荠、Android invalidate/postInvalidate/requestLayout 徹底厘清
7克胳、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅動Handler-Message-Looper解析
9圈匆、Android 鍵盤一招搞定
10漠另、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12臭脓、Android Activity創(chuàng)建到View的顯示過
13酗钞、Android IPC 系列
14、Android 存儲系列
15来累、Java 并發(fā)系列不再疑惑
16砚作、Java 線程池系列
17、Android Jetpack 前置基礎系列
18嘹锁、Android Jetpack 易懂易學系列
19葫录、Kotlin 輕松入門系列
20、Kotlin 協(xié)程系列全面解讀

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末领猾,一起剝皮案震驚了整個濱河市米同,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摔竿,老刑警劉巖面粮,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異继低,居然都是意外死亡熬苍,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門袁翁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柴底,“玉大人,你說我怎么就攤上這事粱胜”ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵焙压,是天一觀的道長鸿脓。 經(jīng)常有香客問我抑钟,道長,這世上最難降的妖魔是什么野哭? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任味赃,我火速辦了婚禮,結果婚禮上虐拓,老公的妹妹穿的比我還像新娘。我一直安慰自己傲武,他們只是感情好蓉驹,可當我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著揪利,像睡著了一般态兴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疟位,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天瞻润,我揣著相機與錄音,去河邊找鬼甜刻。 笑死绍撞,一個胖子當著我的面吹牛,可吹牛的內容都是我干的得院。 我是一名探鬼主播傻铣,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼祥绞!你這毒婦竟也來了非洲?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蜕径,失蹤者是張志新(化名)和其女友劉穎两踏,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兜喻,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡梦染,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了虹统。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弓坞。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖车荔,靈堂內的尸體忽然破棺而出渡冻,到底是詐尸還是另有隱情,我是刑警寧澤忧便,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布族吻,位于F島的核電站帽借,受9級特大地震影響,放射性物質發(fā)生泄漏超歌。R本人自食惡果不足惜砍艾,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望巍举。 院中可真熱鬧脆荷,春花似錦、人聲如沸懊悯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炭分。三九已至桃焕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捧毛,已是汗流浹背观堂。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呀忧,地道東北人师痕。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像荐虐,于是被迫代替她去往敵國和親七兜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內容