協程Flow簡單使用

前言

本文是閱讀協程Flow的總結筆記。

什么是Flow

Kotlin中的Flow API是可以更好的異步處理按順序執行的數據流的方法。

在RxJava中,Observables類型是表示項目流結構的示例。 在訂閱者進行訂閱之前,其主體不會被執行。 訂閱后,訂閱者便開始獲取發射的數據項。 同樣,Flow在相同的條件下工作,即在流生成器內部的代碼到了收集流后才開始運行。

你可以認為他是Kotlin的RxJava,但比RxJava學習成本低,天然與協程友好(協程庫的一部分)

怎樣使用

首先引入kotlin的協程的核心庫

 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'

編寫如下代碼

fun main() =
    runBlocking {
        launch {
            for (k in 1..3) {
                println("I'm not blocked $k")
                delay(100)
            }
        }
        sample().collect {
            println("接收到的消息${it}")
        }
    }


fun sample() = flow<Int> {
    repeat(3) {
        delay(100)
        emit(it)
    }
}

運行如下

I'm not blocked 1
接收到的消息0
I'm not blocked 2
接收到的消息1
I'm not blocked 3
接收到的消息2

Process finished with exit code 0

這就是Flow的基礎用法,需要注意的點如下:
(1)flow是Flow的構造方法,Flow使用emit發射數據,使用collect來接收數據(與RxJava的上下游概念類似)
(2)flow代碼塊是可以掛起的
(3)sample函數沒有用suspend關鍵字進行標識

流是冷的

Flow是冷數據流,表現為構建Flow的代碼中的emit需要在調用collect才開始發送數據。

suspend fun main() = coroutineScope {
    println("收集數據")
    sample1().collect {
        println(it)
    }
    println("再次收集數據")
    sample1().collect {
        println(it)
    }

}

fun sample1() = flow {
    repeat(3) {
        emit(it)
    }
}

代碼運行如下

收集數據
0
1
2
再次收集數據
0
1
2

Process finished with exit code 0

從運行結果可以看到,調用了collect,flow的emit才會調用。

流的超時取消

Flow提供了withTimeoutOrNull來在超時的情況下取消并停止執行其代碼的。
代碼如下:

fun main() = runBlocking {

    withTimeoutOrNull(300) {
        sample2().collect {
            println(it)
        }
    }
    println("完成")

}


fun sample2() = flow {
    repeat(3) {
        kotlinx.coroutines.delay(100)
        emit(it)
    }
}

運行結果如下:

0
1
完成

Process finished with exit code 0

可以看到3沒有打印出來

流構造器

Flow的構造器除了flow{}這種還有:
1.asFlow ,擴展函數,將相關的集合或者序列轉換為流
2.flowOf(...),定義固定發射的數據流
代碼如下:

suspend fun main() = coroutineScope {

    listOf(1, 2, 3).asFlow().collect {
        println(it)
    }

    flowOf(4, 5, 6).collect {
        println(it)
    }

}

運行結果如下:

1
2
3
4
5
6

Process finished with exit code 0

過度操作符

Flow中的過度操作符有Map和Filter兩種,其中Map是把Flow中的數據轉換為另外一種數據類型發射出來。Filter則是將符合條件的數據發送出來。

Map代碼如下:

suspend fun main()= coroutineScope {
    listOf(1, 2, 3).asFlow().map { it ->
        waitAWhile(it)
    }.collect {
        println(it)
    }
}


suspend fun waitAWhile(int: Int): String {
    delay(100)
    return "等了一會$int"
}

代碼將Int類型轉換為String類型的數據發送出來
運行結果如下:

等了一會1
等了一會2
等了一會3

Fliter操作符代碼如下:

suspend fun main()= coroutineScope {
    listOf(1, 2, 3).asFlow().filter {
        it>1
    }.collect {
        println(it)
    }

}

代碼中只發射大于1的數據,也就是2,3
運行結果如下:

2
3

轉換操作符

Flow中的轉換操作符主要是TransForm,可以實現更復雜的變換(時間上map和filter都是基于Transform實現的),代碼如下:

listOf(1, 2, 3).asFlow().transform {
        emit("${it}發射")
        emit("${it}再發射")
    }.collect {
        println(it)
    }

代碼中將發射一次轉換為發射兩次,運行代碼如下:

1發射
1再發射
2發射
2再發射
3發射
3再發射

Process finished with exit code 0

限長操作符

Flow中的限長操作符為Take,在流觸及相應限制的時候會將它的執行取消(與協程一致都是通過拋出異常的方式進行取消,該操作符已經處理了異常拋出)。
代碼如下:

listOf(1, 2, 3).asFlow().take(2).collect {
        println(it)
    }

代碼中設置了限長為2,標識只會接受兩個發射的數據
運行結果如下:

1
2

末端操作符

末端操作符是在流上用于啟動流收集的掛起函數collect 是最基礎的末端操作符,但是還有另外一些更方便使用的末端操作符

  • 轉化為各種集合,例如 toListtoSet
  • 獲取第一個(first)值與確保流發射單個(single)值的操作符。
  • 使用 reducefold 將流規約到單個值。

toList代碼如下:

suspend fun main() {
    val stringList = mutableListOf<String>()
//轉換為list
    flowOf(1,2,3).map {
         "haha$it"
     }.toList(stringList).forEach {
        println(it)
    }
}

運行結果如下:

haha1
haha2
haha3

Process finished with exit code 0

toSet代碼如下:

 flowOf(1, 2, 3).map {
        "haha$it"
    }.toSet(mutableSetOf()).forEach {
        println(it)
    }

運行結果如下:

haha1
haha2
haha3

Process finished with exit code 0

toCollection代碼如下:

 //轉換為collection
    flowOf(1, 2, 3).map {
        "haha$it"
    }.toCollection(mutableSetOf()).forEach {
        println(it)
    }

運行結果如下:

haha1
haha2
haha3

Process finished with exit code 0

first:獲取第一個發射的數據,其余的拋棄
代碼如下:

  val first = flowOf(1, 2, 3).map {
        "haha$it"
    }.first()
    println(first)

運行結果如下:

haha1

Process finished with exit code 0

first可以通過lambda函數作為選擇條件,返回滿足條件的第一個值,代碼如下

   //first 返回第一個滿足條件的元素
    val first = flowOf(1, 2, 3).map {
        "haha$it"
    }.first {
        it.contains("2")
    }
    println(first)

運行結果如下:

haha2

Process finished with exit code 0

single也是返回一個發送的數據,但,他要求至多有一個數據發送,發送多個數據會報錯
代碼如下:

   //single返回單個值
    val single = flowOf(1).map {
        "haha$it"
    }.single()
    println(single)

運行結果如下:

haha1

Process finished with exit code 0

singleOrNull 單個發射返回數值,否則返回null

    //singleOrNull 單個發射返回數值,否則返回null
    val singleOrNull = mutableListOf<Int>().asFlow().map {
        "haha$it"
    }.singleOrNull()
    println(singleOrNull)

運行結果如下:

null

Process finished with exit code 0

reduce 對集合進行計算操作

  val reduce = flowOf(1, 2, 3)
        .reduce { accumulator, value ->
            accumulator + value
        }
    println(reduce)

運行結果如下:

6

Process finished with exit code 0

fold帶初始值的reduce

    //帶初始值的reduce
    val fold = flowOf(1, 2, 3)
        .fold(100) { acc, value ->
            acc + value
        }
    println(fold)

運行結果如下

106

Process finished with exit code 0

流是連續的

流的每次單獨收集都是按順序執行的,除非進行特殊操作的操作符使用多個流(Zip操作符)。該收集過程直接在協程中運行,該協程調用末端操作符。 默認情況下不啟動新協程。 從上游到下游每個過渡操作符都會處理每個發射出的值然后再交給末端操作符。
簡單來說就是一個發射數據執行完所有的操作符(collect除外),下一個數據才能發射并執行相關的操作符
代碼如下:


(1..5).asFlow()
    .filter {
        println("Filter $it")
        it % 2 == 0              
    }              
    .map { 
        println("Map $it")
        "string $it"
    }.collect { 
        println("Collect $it")
    }  

運行結果如下:

Filter 1
Filter 2
Map 2
Collect string 2
Filter 3
Filter 4
Map 4
Collect string 4
Filter 5

流的上下文切換

流的收集總是在調用協程的上下文中發生。如無特殊指定,則默認與所在協程處于同一個上下文中。

suspend fun main() {
    println(Thread.currentThread().name)
        flow {
            println(Thread.currentThread().name)
            emit(1)
            emit(2)
            emit(3)
        }.collect {
            println(Thread.currentThread().name)
            println(it)
        }
}

運行結果如下

main
main
main
1
main
2
main
3

Process finished with exit code 0

但是如果發射器與接收器不處于同一個上下文時則會報錯

 //發射邏輯與接收邏輯不在同一個線程中,會發生報錯
    val flow = flow {
        withContext(Dispatchers.Default) {
            println("發射時的線程:${Thread.currentThread().name}")
            emit(1)
            emit(2)
            emit(3)
        }

    }
    flow.collect {
        println(it)
    }

運行如下:
···
接收時的線程:main
發射時的線程:DefaultDispatcher-worker-1
Exception in thread "main" java.lang.IllegalStateException: Flow invariant is violated:
···
為了實現更高發射器的上下文,可以使用flowOn手動切換所處線程

val flowOn = flow {
        println("發射時的線程:${Thread.currentThread().name}")
        emit(1)
        emit(2)
        emit(3)
    }.flowOn(Dispatchers.Default)


    withContext(Dispatchers.IO) {
        flowOn.collect {
            println("接收時的線程:${Thread.currentThread().name}")
            println(it)
        }
    }

運行結果如下:

發射時的線程:DefaultDispatcher-worker-3
接收時的線程:DefaultDispatcher-worker-1
1
接收時的線程:DefaultDispatcher-worker-1
2
接收時的線程:DefaultDispatcher-worker-1
3

Process finished with exit code 0

緩沖操作符

從收集流所花費的時間來看,將流的不同部分運行在不同的協程中將會很有幫助,特別是當涉及到長時間運行的異步操作時。例如,考慮一種情況, 一個 發射器 流的發射很慢,它每花費 100 毫秒才產生一個元素;而收集器也非常慢, 需要花費 200 毫秒來處理元素。讓我們看看從該流收集三個數字要花費多長時間。

val measureTimeMillis = measureTimeMillis {
        flow {
            repeat(3) {
                delay(100)
                emit(it)
            }
        }.collect {
            delay(200)
            println(it)
        }
    }
    println(measureTimeMillis)

運行結果如下:

0
1
2
1031

Process finished with exit code 0

那么如何加快該過程呢,答案是使用Buffer操作符

 val measureTimeMillis = measureTimeMillis {
        flow {
            repeat(3) {
                delay(100)
                emit(it)
            }
        }.buffer().collect {
            delay(200)
            println(it)
        }
    }
    println(measureTimeMillis)

運行結果如下:

0
1
2
985

它產生了相同的數字,只是更快了,由于我們高效地創建了處理流水線, 僅僅需要等待第一個數字產生的 100 毫秒以及處理每個數字各需花費的 200 毫秒。(PS:上邊的flowOn也用到了類似的緩存機制,所以不需要再次調用buffer操作符)。

合并操作符

當流代表部分操作結果或操作狀態更新時,可能沒有必要處理每個值,而是只處理最新的那個。在本示例中,當收集器處理它們太慢的時候, conflate 操作符可以用于跳過中間值。構建前面的示例:

suspend fun main() {
    flow {
        repeat(3) {
            delay(100)
            emit(it)
        }
    }.conflate().collect {
        delay(500)
        println(it)
    }
}

運行結果如下:

0
2

Process finished with exit code 0

沒有打印中間的1

當發射器和收集器都很慢的時候,合并是加快處理速度的一種方式。它通過刪除發射值來實現。 另一種方式是取消緩慢的收集器,并在每次發射新值的時候重新啟動它。有一組與 xxx 操作符執行相同基本邏輯的 xxxLatest 操作符,但是在新值產生的時候取消執行其塊中的代碼。讓我們在先前的示例中嘗試更換 conflatecollectLatest

suspend fun main() {
    flow {
        repeat(3) {
            kotlinx.coroutines.delay(100)
            emit(it)
        }
    }.collectLatest {
        println(it)
        delay(500)
        println(it)
    }
}

運行結果如下

2

Process finished with exit code 0

只打印出最后的2,沒有執行0,1的打印

組合操作符

就像 Kotlin 標準庫中的 Sequence.zip 擴展函數一樣, 流擁有一個 zip 操作符用于組合兩個流中的相關值(多余數據直接丟棄)

/**
 * zip 組合兩個流中的相關值,多余的進行丟棄
 */
suspend fun main() {
    val flow1 = flowOf(1, 2, 3, 4)
    val flow2 = flowOf("one", "two", "three")
    flow1.zip(flow2) { i: Int, s: String ->
        "$s:$i"
    }.collect {
        println(it)
    }

}

運行結果如下

one:1
two:2
three:3

Process finished with exit code 0

當流表示一個變量或操作的最新值時(請參閱相關小節 conflation),可能需要執行計算,這依賴于相應流的最新值,并且每當上游流產生值的時候都需要重新計算。這種相應的操作符家族稱為 combine

suspend fun main() {
    val flow1 = flowOf(1, 2, 3, 4).onEach { delay(300) }
    val flow2 = flowOf("one", "two", "three").onEach { delay(400) }
    flow1.combine(flow2) { i: Int, s: String ->
        "$s:$i"
    }.collect {
        println(it)
    }
}

運行結果如下:

one:1
one:2
two:2
two:3
three:3
three:4

Process finished with exit code 0

展開操作符

流表示異步接收的值序列,所以很容易遇到這樣的情況: 每個值都會觸發對另一個值序列的請求。比如說,我們可以擁有下面這樣一個返回間隔 500 毫秒的兩個字符串流的函數:

fun reqeustFlow(it: Int) = flow {
    emit("${it}:first")
    kotlinx.coroutines.delay(500)
    emit("${it}:second")
}

現在,如果我們有一個包含三個整數的流,并為每個整數調用 requestFlow

 val map = flowOf(1, 2, 3).map {
        reqeustFlow(it)
    }

然后我們得到了一個包含流的流(Flow<Flow<String>>),需要將其進行展平為單個流以進行下一步處理。集合與序列都擁有 flattenflatMap 操作符來做這件事。然而,由于流具有異步的性質,因此需要不同的展平模式。

flatMapConcat

順序的將發射器發射的數據轉變為一個新的流

 flowOf(1, 2, 3).onEach { delay(100) }.flatMapConcat {
        reqeustFlow(it)
    }.collect {
        println(it)
    }

運行結果如下:

1:first
1:second
2:first
2:second
3:first
3:second

Process finished with exit code 0

flatMapMerge

并發收集所有傳入的流,并將它們的值合并到一個單獨的流,以便盡快的發射值。需要注意的是flatMapMerge 會順序調用代碼塊(本示例中的 { requestFlow(it) }),但是并發收集結果流,相當于執行順序是首先執行 map { requestFlow(it) } 然后在其返回結果上調用 flattenMerge從現象上來說就是:先獲取所有的發射項,然后每個發射項逐次調用flatMapMerge中的代碼(flatMapMerge有a,b代碼,所有發射項先執行a,執行完后所有發射項再執行b)。

  flowOf(1, 2, 3).onEach { delay(100) }.flatMapMerge {
        reqeustFlow(it)
    }.collect {
        println(it)
    }

運行代碼如下:

1:first
2:first
3:first
1:second
2:second
3:second

Process finished with exit code 0

flatMapLatest

在發出新流后立即取消先前流的收集

flowOf(1, 2, 3).onEach { delay(100) }.flatMapLatest {
        reqeustFlow(it)
    }.collect {
        println(it)
    }

運行結果如下:

1:first
2:first
3:first
3:second

Process finished with exit code 0

try-catch

flow在收集器代碼塊使用try-catch的話,會接收到flow所有的異常拋出。我們首先看發射器發生錯誤的時候

 val fl = flow {
        emit(1)
        emit(2)
        emit(3)
    }.map {
        check(it <3) {
            "上游發生錯誤"
        }
        it
    }

    try {
        fl.collect {
            println(it)
        }
    } catch (e: Exception) {
        println(e.message)
    }

運行結果如下:

1
2
上游發生錯誤

Process finished with exit code 0

這里需要注意的是check操作符,如果不符合條件才會執行大括號內的代碼,并將返回值作為異常的消息。
如果下游發生異常的話,是否能捕獲到異常呢?,看如下代碼

 val flowOf = flowOf(1, 2, 3)
    try {
        flowOf.collect {
            check(it <= 2) {
                "下游發生錯誤"
            }
            println(it)
        }
    } catch (e: Exception) {
        println(e.message)
    }

運行結果如下:

1
2
下游發生錯誤

Process finished with exit code 0

可以看到如果用try-catch包圍收集器的話是可以捕獲到上下游的異常的,需要注意的是流必須對異常透明,即在 flow { ... } 構建器內部的 try/catch 塊中發射值是違反異常透明性的。這樣可以保證收集器拋出的一個異常能被像先前示例中那樣的 try/catch 塊捕獲。那么Flow有沒有專門的捕獲異常的操作符呢,答案是有的,他就是catch操作符。

 flowOf(1, 2, 3).map {
        check(it <= 2) {
            "上游出現錯誤"
        }
        it
    }.catch {
        println(it.message)
    }.collect {
        println(it)
    }

運行結果如下:

1
2
上游出現錯誤

Process finished with exit code 0

但是這樣是不能獲取到接收器發生的錯誤的。如果想捕獲發射器收集器的異常,可采取如下的方法

    flowOf(1, 2, 3).map {
        check(it < 3) {
            "上游發生了錯誤"
        }
        it
    }.onEach {
//放開該注釋,注釋掉上面的check代碼,則能捕獲下游發生的錯誤
//        check(it < 2) {
//            "下游發生錯誤"
//        }
        println(it)

    }.catch {
        println(it.message)
    }.collect()

運行結果如下:

1
2
上游發生了錯誤

Process finished with exit code 0

至于你采用try-catch還是catch操作符,那就取決于你的習慣了。

流完成

流的完成也有finally和onCompletion操作符兩種。先看finally這種吧

val map = flowOf(1, 2, 3).map {
        check(it < 3) {
            "上游發生錯誤"
        }
        it
    }

    try {
        map.collect {
            println(it)
        }
    } catch (e: Exception) {
        println(e.message)
    }finally {
        println("結束了")
    }

運行結果如下

1
2
上游發生錯誤
結束了

Process finished with exit code 0

onCompletion操作符如下:

flowOf(1, 2, 3).map {
        check(it <= 2) {
            "上游發生錯誤"
        }
        it
    }.catch {
        println(it.message)
    }.onCompletion {
        if (it != null) {
            println("發生了異常")
        } else {
            println("沒有異常")
        }

    }.collect {
        println(it)
    }

運行結果

1
2
上游發生錯誤
沒有異常

Process finished with exit code 0

這里需要注意的是onComplettion是可以捕獲到相關異常的,但他不能處理異常,異常仍會向下游流動。本文中的代碼因為在onCompletion之前調用了catch,捕獲了異常,所以
onCompletion不會收到相關異常信息。

啟動流

使用流表示來自一些源的異步事件是很簡單的。 在這個案例中,我們需要一個類似 addEventListener 的函數,該函數注冊一段響應的代碼處理即將到來的事件,并繼續進行進一步的處理。onEach 操作符可以擔任該角色。 然而,onEach 是一個過渡操作符。我們也需要一個末端操作符來收集流。 否則僅調用 onEach 是無效的。

如果我們在 onEach 之后使用 collect 末端操作符,那么后面的代碼會一直等待直至流被收集:

// 模仿事件流
fun events(): Flow<Int> = (1..3).asFlow().onEach { delay(100) }

fun main() = runBlocking<Unit> {
    events()
        .onEach { event -> println("Event: $event") }
        .collect() // <--- 等待流收集
    println("Done")
}       

運行結果

Event: 1
Event: 2
Event: 3
Done

launchIn 末端操作符可以在這里派上用場。使用 launchIn 替換 collect 我們可以在單獨的協程中啟動流的收集,這樣就可以立即繼續進一步執行代碼:

fun main() = runBlocking<Unit> {
    events()
        .onEach { event -> println("Event: $event") }
        .launchIn(this) // <--- 在單獨的協程中執行流
    println("Done")
}      
Done
Event: 1
Event: 2
Event: 3

launchIn 必要的參數 CoroutineScope 指定了用哪一個協程來啟動流的收集。在先前的示例中這個作用域來自 runBlocking 協程構建器,在這個流運行的時候,runBlocking 作用域等待它的子協程執行完畢并防止 main 函數返回并終止此示例。

在實際的應用中,作用域來自于一個壽命有限的實體。在該實體的壽命終止后,相應的作用域就會被取消,即取消相應流的收集。這種成對的 onEach { ... }.launchIn(scope) 工作方式就像 addEventListener 一樣。而且,這不需要相應的 removeEventListener 函數, 因為取消與結構化并發可以達成這個目的。

注意,launchIn 也會返回一個 Job,可以在不取消整個作用域的情況下僅取消相應的流收集或對其進行 join

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容