kotlin-協(xié)程的異常處理

在 Kotlin 協(xié)程當(dāng)中,我們通常把異常分為兩大類喇澡,一類是取消異常(CancellationException)殊校,另一類是其他異常呕屎。之所以要這么分類秀睛,是因?yàn)樵?Kotlin 協(xié)程當(dāng)中蹂安,這兩種異常的處理方式是不一樣的田盈≡是疲或者說,在 Kotlin 協(xié)程所有的異常當(dāng)中畦韭,我們需要把CancellationException 單獨(dú)拎出來廊驼,特殊對(duì)待妒挎。

當(dāng)協(xié)程任務(wù)被取消的時(shí)候酝掩,協(xié)程內(nèi)部是會(huì)產(chǎn)生一個(gè) CancellationException 期虾。很多初學(xué)者都會(huì)遇到一個(gè)問題镶苞,那就是協(xié)程無法被取消茂蚓。帶著這個(gè)問題進(jìn)入下面的內(nèi)容晾浴。

一脊凰、協(xié)程的取消需要內(nèi)部配合

先看看下面的例子

fun main() {
    runBlocking {
        printMsg("start")
        val job = launch(Dispatchers.IO) {
            var i = 0
            while (true) {
                Thread.sleep(500L)
                i++
                printMsg("i = $i")
            }
        }
        delay(2000L)
        job.cancel()   <------2秒后在協(xié)程作用域內(nèi)取消協(xié)程
        job.join()
        printMsg("end")
    }
}

//日志
main @coroutine#1 start
DefaultDispatcher-worker-1 @coroutine#2 i = 1
DefaultDispatcher-worker-1 @coroutine#2 i = 2
DefaultDispatcher-worker-1 @coroutine#2 i = 3
//停不下來了
......

為什么2秒后協(xié)程沒有退出呢?這是因?yàn)閰f(xié)程是協(xié)作式的最岗,我們?cè)谧饔糜騼?nèi)調(diào)用了cancel方法惶楼,協(xié)程需要自己檢查取消狀態(tài)歼捐,并在適當(dāng)?shù)臅r(shí)機(jī)主動(dòng)做出響應(yīng)豹储。取消狀態(tài)可以通過協(xié)程的 isActive 屬性進(jìn)行檢查剥扣。但是在我們的代碼中,由于是無限循環(huán)曙聂,協(xié)程沒有時(shí)機(jī)主動(dòng)檢查取消狀態(tài)断国,因此協(xié)程無法感知到取消請(qǐng)求并退出稳衬。

改造上面的代碼

fun main() {
    runBlocking {
        printMsg("start")
        val job = launch(Dispatchers.IO) {
            var i = 0
            while (isActive) {      <----------方式一:主動(dòng)提供檢查的時(shí)機(jī)
                Thread.sleep(500L)
                //delay(500L)    <----------方式二:掛起函數(shù),在掛起點(diǎn)檢查自己的狀態(tài)
                i++
                printMsg("i = $i")
            }
        }
        delay(2000L)
        job.cancel()
        job.join()
        printMsg("end")
    }
}

//日志


提供了二種方式:

  • 方式一:通過isActive主動(dòng)發(fā)起狀態(tài)檢查弄砍。

  • 方式二:將sleep改為掛起函數(shù)delay,因?yàn)閽炱鸷瘮?shù)在掛起或恢復(fù)的時(shí)候肯定會(huì)檢查協(xié)程的狀態(tài)(比如協(xié)程已經(jīng)被cancel肯定不會(huì)再從掛起恢復(fù)了)莱坎。

綜上檐什,協(xié)程代碼如果無法被cancel乃正,請(qǐng)檢查協(xié)程是否有檢查狀態(tài)的時(shí)機(jī)瓮具。

二名党、不要打破協(xié)程的父子結(jié)構(gòu)

看下面的例子

var startTime: Long = 0
fun main() {
    runBlocking {
        startTime = System.currentTimeMillis()
        printMsg("start")
        var childJob1: Job? = null
        var childJob2: Job? = null
        val parentJob = launch(Dispatchers.IO) {
            childJob1 = launch {        <------子協(xié)程使用父協(xié)程的上下文
                printMsg("childJob1 start")
                delay(600L)          <------子協(xié)程掛起600毫秒后執(zhí)行完
                printMsg("childJob1 end")
            }

            childJob2 = launch(Job()) {       <------子協(xié)程使用自己的上下文
                printMsg("childJob2 start")
                delay(600L)          <------子協(xié)程掛起600毫秒后執(zhí)行完
                printMsg("childJob2 end")
            }
        }

        delay(400L)
        parentJob.cancel()    <---------400毫秒后取消父協(xié)程
        printMsg("childJob1.isActive=${childJob1?.isActive}")   <-----程序執(zhí)行完時(shí)打印子協(xié)程1的狀態(tài)
        printMsg("childJob2.isActive=${childJob2?.isActive}")   <-----程序執(zhí)行完時(shí)打印子協(xié)程2的狀態(tài)
        printMsg("end")       <-----程序執(zhí)行完時(shí)
    }
}

fun printMsg(msg: Any) {
    println("打印內(nèi)容:$msg 消耗時(shí)間:${System.currentTimeMillis() - startTime} 線程信息:${Thread.currentThread().name} ")
}

//日志
打印內(nèi)容:start 消耗時(shí)間:0 線程信息:main @coroutine#1 
打印內(nèi)容:childJob1 start 消耗時(shí)間:15 線程信息:DefaultDispatcher-worker-3 @coroutine#3 
打印內(nèi)容:childJob2 start 消耗時(shí)間:22 線程信息:DefaultDispatcher-worker-2 @coroutine#4 
打印內(nèi)容:childJob1.isActive=false 消耗時(shí)間:430 線程信息:main @coroutine#1 
打印內(nèi)容:childJob2.isActive=true 消耗時(shí)間:430 線程信息:main @coroutine#1    <------程序執(zhí)行完時(shí),子協(xié)程2并沒有退出欧啤,isActive=true
打印內(nèi)容:end 消耗時(shí)間:430 線程信息:main @coroutine#1 
Process finished with exit code 0

代碼并不難邢隧,注釋也很詳細(xì)府框∑染福可以看到子協(xié)程2使用自己的上下文后脫離了父協(xié)程的控制系宜,當(dāng)父協(xié)程被cancel后盹牧,子協(xié)程2并沒有被cancel口柳,isActive狀態(tài)仍然是true跃闹。

所以望艺,不要打破協(xié)程的父子結(jié)構(gòu)找默!

三惩激、不要用 try-catch 直接包裹 launch、async

看下面的例子

fun main() {
    runBlocking {
        printMsg("start")
        try {
            printMsg("try start")
            launch {
                printMsg("launch start")
                delay(200L)
                1 / 0          <------------200毫秒后創(chuàng)建一個(gè)異常
                printMsg("launch end")
            }
            printMsg("try end")
        } catch (exception: Exception) {
            printMsg("catch $exception")
        }
        printMsg("end")
    }
}

//日志
main @coroutine#1 start
main @coroutine#1 try start
main @coroutine#1 try end
main @coroutine#1 end
main @coroutine#2 launch start
Exception in thread "main" java.lang.ArithmeticException: / by zero    <-----報(bào)錯(cuò)程序崩潰

雖然try-catch包裹了協(xié)程的內(nèi)容魄咕,但是程序還是報(bào)錯(cuò)哮兰,這是因?yàn)樽訁f(xié)程與父協(xié)程是并發(fā)執(zhí)行的喝滞,它們之間是獨(dú)立的執(zhí)行流程右遭,所以上面代碼中父協(xié)程的 try-catch 無法捕獲子協(xié)程拋出的異常窘哈。

try-catch修改上面的代碼

fun main() {
    runBlocking {
        printMsg("start")
        launch {
            printMsg("launch start")
            try {
                printMsg("try start")
                delay(200L)
                1 / 0
                printMsg("try end")
            } catch (exception: Exception) {
                printMsg("catch $exception")
            }
            printMsg("launch end")
        }
        printMsg("end")
    }
}

//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 launch start
main @coroutine#2 try start
main @coroutine#2 catch java.lang.ArithmeticException: / by zero    <------異常被成功捕獲
main @coroutine#2 launch end
Process finished with exit code 0

如果使用async創(chuàng)建協(xié)程滚婉,try-catch是應(yīng)該包裹async內(nèi)的代碼塊還是應(yīng)該包裹deferred.await()? 寫段代碼看看

fun main() {
    runBlocking {
        printMsg("start")
        val deferred = async() {
            printMsg("async start")
            delay(200L)
            1 / 0
            printMsg("async end")
        }

        try {
            deferred.await()
        } catch (exception: Exception) {
            printMsg("catch $exception")
        }

        printMsg("end")
    }
}

//日志
main @coroutine#1 start
main @coroutine#2 async start
main @coroutine#1 catch java.lang.ArithmeticException: / by zero     <------捕獲到了異常
main @coroutine#1 end
Exception in thread "main" java.lang.ArithmeticException: / by zero   <-----報(bào)錯(cuò)程序崩潰

雖然捕獲到了異常远剩,但是程序還是報(bào)錯(cuò)了瓜晤,所以try-catch一般還是包裹具體的代碼塊吧痢掠。

四、使用SupervisorJob

上面的一段代碼try-catchdeferred.await()仍然報(bào)錯(cuò)蛔钙,有沒有辦法補(bǔ)救這段代碼呢吁脱?答案是有兼贡,可以使用SupervisorJob()遍希。代碼如下:

fun main() {
    runBlocking {
        printMsg("start")
        val deferred = async(SupervisorJob()) {      <-------變化在這里
            printMsg("async start")
            delay(200L)
            1 / 0
            printMsg("async end")
        }

        try {
            deferred.await()
        } catch (exception: Exception) {
            printMsg("catch $exception")
        }

        printMsg("end")
    }
}

//日志
main @coroutine#1 start
main @coroutine#2 async start
main @coroutine#1 catch java.lang.ArithmeticException: / by zero
main @coroutine#1 end
Process finished with exit code 0

為什么加了SupervisorJob()就不報(bào)錯(cuò)了? 看下SupervisorJob()的源碼:

@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

public interface CompletableJob : Job {
    
    public fun complete(): Boolean

    public fun completeExceptionally(exception: Throwable): Boolean
}

SupervisorJob() 其實(shí)不是構(gòu)造函數(shù)胁黑,它只是一個(gè)普通的頂層函數(shù)漂洋。而這個(gè)方法返回的對(duì)象刽漂,是 Job 的子類爽冕。默認(rèn)的 Job 類型會(huì)將異常傳播給父協(xié)程颈畸,如果一個(gè)子協(xié)程拋出異常眯娱,它會(huì)取消父協(xié)程及其所有兄弟協(xié)程徙缴。

通過使用 SupervisorJob疏叨,我們可以創(chuàng)建一個(gè)具有獨(dú)立異常處理行為的作業(yè)層級(jí)穿剖。這意味著即使子協(xié)程中發(fā)生異常秀又,父協(xié)程仍然可以繼續(xù)執(zhí)行而不會(huì)被取消贬芥,從而避免整個(gè)程序崩潰蘸劈。

SupervisorJob()可以作為 CoroutineScope 的上下文威沫,但是它的監(jiān)管范圍并不是無限大的救巷,看下面的例子:

fun main() {
    runBlocking {
        val supervisorJob = SupervisorJob()    
        val scope = CoroutineScope(coroutineContext + supervisorJob)    <-----作用域內(nèi)使用SupervisorJob()
        val job = scope.launch {              <----注意這里,作用域內(nèi)啟動(dòng)子協(xié)程
            launch {                   <----注意這里,作用域內(nèi)啟動(dòng)孫協(xié)程
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")      <----關(guān)注這個(gè)日志
            }
        }
        job.join()
        scope.cancel()
    }
}

//日志
main @coroutine#3 job1 start
main @coroutine#4 job2 start
Exception in thread "main @coroutine#4" java.lang.ArithmeticException: by zero
Process finished with exit code 0

上面的日志中并沒有輸出job2 end浦译,說明上面job1的異常影響了下面協(xié)程job2的執(zhí)行,那如何修改呢叹俏?

fun main() {
    runBlocking {
        val supervisorJob = SupervisorJob()
        val scope = CoroutineScope(coroutineContext + supervisorJob)
        scope.apply {               <----------變化在這里,launch改為apply
            val job1 = launch {
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            val job2 = launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
            job1.join()       <----------變化在這里
            job2.join()
        }
        scope.cancel()
    }
}

//日志
main @coroutine#2 job1 start
main @coroutine#3 job2 start
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: by zero
main @coroutine#3 job2 end        <--------成功輸出: job2 end
Process finished with exit code 0

可以看到當(dāng)將 SupervisorJob 作為 CoroutineScope 的上下文時(shí)屡谐,它的監(jiān)管范圍僅限于該作用域內(nèi)部啟動(dòng)的子協(xié)程愕掏。

SupervisorJob的源碼中是因?yàn)橹貙懥?code>childCancelled方法并直接返回false饵撑,保證異常不會(huì)向父協(xié)程和其他子協(xié)程傳遞:

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

事實(shí)上kotlin有提供給我們含SupervisorJob上下文的協(xié)程作用域,它就是supervisorScope语卤,源碼如下:


/**
 * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * context's [Job] with [SupervisorJob].
 * This function returns as soon as the given block and all its child coroutines are completed.
 *
 * Unlike [coroutineScope], a failure of a child does not cause this scope to fail and does not affect its other children,
 * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for additional details.
 * A failure of the scope itself (exception thrown in the [block] or external cancellation) fails the scope with all its children,
 * but does not cancel parent job.
 *
 * The method may throw a [CancellationException] if the current job was cancelled externally,
 * or rethrow an exception thrown by the given [block].
 */
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)      <-------SupervisorCoroutine
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

private class SupervisorCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
    override fun childCancelled(cause: Throwable): Boolean = false    <-------同樣重寫了childCancelled方法返回false
}

我們使用supervisorScope改造上面的代碼:

fun main() {
    runBlocking {
        supervisorScope {
            val job1 = launch {
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            val job2 = launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
            job1.join()
            job2.join()
        }
    }
}

//日志
main @coroutine#2 job1 start
main @coroutine#3 job2 start
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: by zero
main @coroutine#3 job2 end        <--------成功輸出: job2 end
Process finished with exit code 0

五蓖宦、CoroutineExceptionHandler

有時(shí)候由于協(xié)程嵌套的層級(jí)很深稠茂,并且也不需要每一個(gè)協(xié)程去處理異常睬关,這時(shí)候CoroutineExceptionHandler就可以派上用場(chǎng)了,如下:

fun main() {
    runBlocking {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            printMsg("CoroutineExceptionHandler $throwable")
        }
        val scope = CoroutineScope(coroutineExceptionHandler)
        val job = scope.launch {
            launch {
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
        }
        job.join()
        scope.cancel()
    }
}

//日志
DefaultDispatcher-worker-2 @coroutine#3 job1 start
DefaultDispatcher-worker-3 @coroutine#4 job2 start
DefaultDispatcher-worker-3 @coroutine#4 CoroutineExceptionHandler java.lang.ArithmeticException: by zero
Process finished with exit code 0

CoroutineExceptionHandler中成功輸出了異常的日志毡证。試試把CoroutineExceptionHandler放在子協(xié)程報(bào)錯(cuò)的地方有什么樣的結(jié)果?

fun main() {
    runBlocking {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            printMsg("CoroutineExceptionHandler $throwable")
        }
        val scope = CoroutineScope(coroutineContext)       <--------變化在這里
        val job = scope.launch {
            launch(coroutineExceptionHandler) {       <--------變化在這里
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
        }
        job.join()
        scope.cancel()
    }
}

//日志
main @coroutine#3 job1 start
main @coroutine#4 job2 start
Exception in thread "main" java.lang.ArithmeticException: by zero      <-------程序報(bào)錯(cuò)
Process finished with exit code 1

程序報(bào)錯(cuò)电爹,且coroutineExceptionHandler并沒有捕獲到異常,說明coroutineExceptionHandler并沒有起到作用料睛,原因是CoroutineExceptionHandler 只在頂層的協(xié)程當(dāng)中才會(huì)起作用丐箩,當(dāng)子協(xié)程當(dāng)中出現(xiàn)異常以后,它們都會(huì)統(tǒng)一上報(bào)給頂層的父協(xié)程恤煞,然后由頂層的父協(xié)程去調(diào)用 CoroutineExceptionHandler來處理異常屎勘。

看上面的日志都沒有輸出job2 end,說明job1的異常影響到了job2的執(zhí)行居扒,那如果既想用coroutineExceptionHandler兜底異常喜喂,又不想?yún)f(xié)程間因?yàn)楫惓铛绰;ハ嘤绊懺趺崔k呢这嚣? 我們可以試試這樣寫:

fun main() {
    runBlocking {
        val supervisorJob = SupervisorJob()        <----------使用SupervisorJob()
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            printMsg("CoroutineExceptionHandler $throwable")
        }
        val scope = CoroutineScope(coroutineExceptionHandler + supervisorJob)    <-------加入到作用域的上下文
        scope.apply {
            val job1 = launch {
                printMsg("job1 start")
                delay(100L)
                throw  NullPointerException("parameters is null")     <-----子協(xié)程的異常
            }

            val job2 = launch {
                printMsg("job2 start")
                delay(200L)
                launch {                 <-----孫協(xié)程
                    try {
                        1 / 0            <-----孫協(xié)程的異常
                    } catch (exception: ArithmeticException) { 
                        throw  ArithmeticException("by zero")     <------記得拋出來,不拋出來也沒有的
                    }
                }
            }

            val job3 = launch {
                printMsg("job3 start")
                delay(300L)
                printMsg("job3 end")
            }

            job1.join()
            job2.join()
            job3.join()
        }
        scope.cancel()
    }
}

//日志
DefaultDispatcher-worker-1 @coroutine#2 job1 start
DefaultDispatcher-worker-2 @coroutine#3 job2 start
DefaultDispatcher-worker-3 @coroutine#4 job3 start
DefaultDispatcher-worker-2 @coroutine#2 CoroutineExceptionHandler java.lang.NullPointerException: parameters is null
DefaultDispatcher-worker-3 @coroutine#5 CoroutineExceptionHandler java.lang.ArithmeticException: by zero
DefaultDispatcher-worker-3 @coroutine#4 job3 end
Process finished with exit code 0

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件梧乘,死亡現(xiàn)場(chǎng)離奇詭異哮洽,居然都是意外死亡莺葫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門烤镐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悴灵,“玉大人茫孔,你說我怎么就攤上這事剩晴≌雷螅” “怎么了鲁纠?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)帜慢。 經(jīng)常有香客問我允青,道長(zhǎng)停撞,這世上最難降的妖魔是什么翅萤? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任笤受,我火速辦了婚禮,結(jié)果婚禮上搪花,老公的妹妹穿的比我還像新娘许师。我一直安慰自己,他們只是感情好桌肴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布的猛。 她就那樣靜靜地躺著裙椭,像睡著了一般弊攘。 火紅的嫁衣襯著肌膚如雪竟宋。 梳的紋絲不亂的頭發(fā)上挪捕,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天发绢,我揣著相機(jī)與錄音介杆,去河邊找鬼韭寸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛褒脯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沉御,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼非春,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼得哆!你這毒婦竟也來了贩据?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤闸餐,失蹤者是張志新(化名)和其女友劉穎饱亮,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舍沙,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡近上,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了场勤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戈锻。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖和媳,靈堂內(nèi)的尸體忽然破棺而出格遭,到底是詐尸還是另有隱情,我是刑警寧澤留瞳,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布拒迅,位于F島的核電站,受9級(jí)特大地震影響她倘,放射性物質(zhì)發(fā)生泄漏璧微。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一硬梁、第九天 我趴在偏房一處隱蔽的房頂上張望前硫。 院中可真熱鬧,春花似錦荧止、人聲如沸屹电。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽危号。三九已至,卻和暖如春素邪,著一層夾襖步出監(jiān)牢的瞬間外莲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來泰國打工兔朦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留偷线,地道東北人磨确。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像淋昭,于是被迫代替她去往敵國和親俐填。 傳聞我的和親對(duì)象是個(gè)殘疾皇子安接,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355

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