Kotlin Coroutines(協(xié)程)講解

前言

翻譯好的文章也是一種學習方式

原文標題:Coroutines in Kotlin 1.3 explained: Suspending functions, contexts, builders and scopes

原文作者: Antonio Leiva

協(xié)程簡介

協(xié)程是 Kotlin 的一大特色兔综。使用協(xié)程,可以簡化異步編程如孝,使代碼可讀性更好岁诉、更容易理解。

使用協(xié)程,不同于傳統(tǒng)的回調(diào)方式谓苟,可以使用同步的方式編寫異步代碼官脓。同步方法返回的結果就是異步請求的結果。

協(xié)程到底有什么魔法涝焙?馬上為您揭曉卑笨。在這之前,我們需要知道為什么協(xié)程這么重要仑撞。

Kotlin 1.1 中 協(xié)程作為實驗特性湾趾,到現(xiàn)在 Kotlin 1.3 發(fā)布了最終的 API,協(xié)程已經(jīng)可以用于生產(chǎn)環(huán)境中派草。

協(xié)程的目標:先看一下現(xiàn)存的一些問題

獲取文中的完整示例點擊 這里

假設要做一個登陸界面如下圖:

login.png

用戶輸入用戶名和密碼搀缠,然后點擊登陸。

假設是這樣的流程:App 首先請求服務器校驗用戶名和密碼近迁,校驗成功后艺普,然后請求該用戶的好友列表。

偽代碼如下:

progress.visibility = View.VISIBLE

userService.doLoginAsync(username, password) { user ->

    userService.requestCurrentFriendsAsync(user) { friends ->

        val finalUser = user.copy(friends = friends)
        toast("User ${finalUser.name} has ${finalUser.friends.size} friends")

        progress.visibility = View.GONE
    }

}

步驟如下:

  1. 顯示一個進度條鉴竭;
  2. 請求服務器校驗用戶名和密碼歧譬;
  3. 等待校驗成功后,請求服務器獲取好友列表搏存;
  4. 最后瑰步,隱藏進度條;

情況還可以更復雜璧眠,想象一下缩焦,不僅要請求好友列表,還需要請求推薦好友列表责静,并把兩次結果合并進一個列表袁滥。

有兩種選擇:

  1. 最簡單的方式就是,在請求完好友列表之后灾螃,再請求推薦好友列表题翻,但是這種方式不夠高效,因為后者并不依賴前者的請求結果腰鬼;
  2. 這種方式相對復雜一些嵌赠,同時請求好友列表和推薦好友列表,并同步兩次請求的結果熄赡;

通常情況下姜挺,想要偷懶的人可能會選擇第一種方式:

progress.visibility = View.VISIBLE

userService.doLoginAsync(username, password) { user ->

    userService.requestCurrentFriendsAsync(user) { currentFriends ->

        userService.requestSuggestedFriendsAsync(user) { suggestedFriends ->
            val finalUser = user.copy(friends = currentFriends + suggestedFriends)
            toast("User ${finalUser.name} has ${finalUser.friends.size} friends")

            progress.visibility = View.GONE
        }

    }

}

到這里,代碼開始變得復雜了本谜,出現(xiàn)了可怕的回調(diào)地獄:后一個請求總是嵌套在前一個請求的結果回調(diào)里面初家,縮進變得越來越多偎窘。

由于使用的是 Kotlinlambdas乌助,可能看起來并沒有那么糟糕溜在。但是隨著請求的增多,代碼變得越來越難以管理他托。

別忘了掖肋,我們使用的還是一種相對簡單但并不高效的一種方式。

什么是協(xié)程(Coroutine

簡單來說赏参,協(xié)程像是輕量級的線程志笼,但并不完全是線程。

首先把篓,協(xié)程可以讓你順序地寫異步代碼纫溃,極大地降低了異步編程帶來的負擔;

其次韧掩,協(xié)程更加高效紊浩。多個協(xié)程可以共用一個線程。一個 App 可以運行的線程數(shù)是有限的疗锐,但是可以運行的協(xié)程數(shù)量幾乎是無限的坊谁;

協(xié)程實現(xiàn)的基礎是可中斷的方法(suspending functions)』可中斷的方法可以在任意的地方中斷協(xié)程的執(zhí)行口芍,直到該可中斷的方法返回結果或者執(zhí)行完成。

運行在協(xié)程中的可中斷的方法(通常情況下)不會阻塞當前線程雇卷,之所以是通常情況下鬓椭,因為這取決于我們的使用方式。具體下面會講到关划。

coroutine {
    progress.visibility = View.VISIBLE

    val user = suspended { userService.doLogin(username, password) }
    val currentFriends = suspended { userService.requestCurrentFriends(user) }

    val finalUser = user.copy(friends = currentFriends)
    toast("User ${finalUser.name} has ${finalUser.friends.size} friends")

    progress.visibility = View.GONE
}

上面的示例是協(xié)程的常用使用范式膘融。首先,使用一個協(xié)程構造器(coroutine builder)創(chuàng)建一個協(xié)程祭玉,然后氧映,一個或多個可中斷的方法運行在協(xié)程中,這些方法將會中斷協(xié)程的執(zhí)行脱货,直到它們返回結果岛都。

可中斷的方法返回結果后,我們在下一行代碼就可以使用這些結果振峻,非常像順序編程臼疫。注意實際上 Kotlin 中并不存在 coroutinesuspended 這兩個關鍵字,上述示例只是為了便于演示協(xié)程的使用范式扣孟。

可中斷的方法(suspending functions

可中斷的方法有能力中斷協(xié)程的執(zhí)行烫堤,當可中斷的方法執(zhí)行完畢后,接著就可以使用它們返回的結果。

val user = suspended { userService.doLogin(username, password) }
val currentFriends = suspended { userService.requestCurrentFriends(user) }

可中斷的方法可以運行在相同的或不同的線程鸽斟,這取決于你的使用方式拔创。可中斷的方法只能運行在協(xié)程中或其他可中斷的方法中富蓄。

聲明一個可中斷的方法剩燥,只需要使用 suspend 保留字:

suspend fun suspendingFunction() : Int  {
    // Long running task
    return 0
}

回到最初的示例,你可能會問上述代碼運行在哪個線程立倍,我們先看這一行代碼:

coroutine {
    progress.visibility = View.VISIBLE
    ...
}

你認為這行代碼運行在哪個線程呢灭红?你確定它是運行在 UI 線程嗎?如果不是口注,App 就會崩潰变擒,所以弄明白運行在哪個線程很重要。

答案就是這取決于協(xié)程上下文(coroutine context的設置寝志。

協(xié)程上下文(Coroutine Context

協(xié)程上下文是一系列規(guī)則和配置的集合赁项,它決定了協(xié)程的運行方式。也可以理解為澈段,它包含了一系列的鍵值對悠菜。

現(xiàn)在,你只需要知道 dispatcher 是其中的一個配置败富,它可以指定協(xié)程運行在哪個線程悔醋。

dispatcher 有兩種方式可以配置:

  1. 明確指定需要使用的 dispatcher;
  2. 由協(xié)程作用域(coroutine scope)決定。這里先不展開說兽叮,后面會詳細說明芬骄;

具體來說,協(xié)程構造器(coroutine builder)接收一個協(xié)程上下文(coroutine context)作為第一個參數(shù)鹦聪,我們可以傳入要使用的 dispatcher账阻。因為 dispatcher 實現(xiàn)了協(xié)程上下文,所以可以作為參數(shù)傳入:

coroutine(Dispatchers.Main) {
    progress.visibility = View.VISIBLE
    ...
}

現(xiàn)在泽本,改變進度條可見性的代碼就運行在了 UI 線程淘太。不僅如此,協(xié)程內(nèi)的所有代碼都運行在 UI 線程规丽。那么問題來了蒲牧,可中斷的方法會怎么運行?

coroutine {
    ...
    val user = suspended { userService.doLogin(username, password) }
    val currentFriends = suspended { userService.requestCurrentFriends(user) }
    ...
}

這些請求服務的代碼也是運行在主線程嗎赌莺?如果真是這樣的話冰抢,它們會阻塞主線程。到底是不是呢艘狭,還是那句話挎扰,這取決于你的使用方式翠订。

可中斷的方法有多種辦法配置要使用的 dispatcher营密,其中最常用的方法是 withContext缓醋。

withContext

在協(xié)程內(nèi)部,這個方法可以輕易地改變代碼運行時所在的上下文贸营。它是一個可中斷的方法骇吭,所以調(diào)用它會中斷協(xié)程的執(zhí)行橙弱,直到該方法執(zhí)行完成歧寺。

這樣以來燥狰,我們就可以讓示例中那些可中斷的方法運行在不同的線程中:

suspend fun suspendLogin(username: String, password: String) =
        withContext(Dispatchers.Main) {
            userService.doLogin(username, password)
        }

上面這些代碼會運行在主線程,所以仍然會阻塞 UI 斜筐。但是龙致,現(xiàn)在我們可以輕易地指定使用不同的 dispatcher:

suspend fun suspendLogin(username: String, password: String) =
        withContext(Dispatchers.IO) {
            userService.doLogin(username, password)
        }

現(xiàn)在我們使用了 IO dispatcher, 上述代碼會運行在子線程。另外顷链,withContext 本身就是一個可中斷的方法目代,所以,我們沒必要讓它運行在另一個可中斷方法中嗤练。所以我們也可以這樣寫:

val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
val currentFriends = withContext(Dispatchers.IO) { userService.requestCurrentFriends(user) }

目前為止榛了,我們認識了兩個 dispatcher,下面我們詳細介紹一下所有的 dispatcher 的使用場景煞抬。

  • Default: 當我們未指定 dispatcher 的時候會默認使用霜大,當然,我們也可以明確設置使用它革答。它一般用于 CPU 密集型的任務,特別是涉及到計算战坤、算法的場景。它可以使用和 CPU 核數(shù)一樣多的線程残拐。正因為是密集型的任務途茫,同時運行多個線程并沒有意義,因為 CPU 將會很繁忙溪食。

  • IO: 它用于輸入/輸出的場景囊卜。通常,涉及到會阻塞線程错沃,需要等待另一個系統(tǒng)響應的任務边败,比如:網(wǎng)絡請求、數(shù)據(jù)庫操作捎废、文件讀寫等笑窜,都可以使用它。因為它不使用 CPU 登疗,可以同一時間運行多個線程排截,默認是數(shù)量為 64 的線程池嫌蚤。Android App 中有很多網(wǎng)絡請求的操作,所以你可能會經(jīng)常用到它断傲。

  • UnConfined: 如果你不在乎啟動了多少個線程脱吱,那么你可以使用它。它使用的線程是不可控制的认罩,除非你特別清楚你在做什么箱蝠,否則不建議使用它。

  • Main: 這是 UI 相關的協(xié)程庫里面的一個 dispatcher垦垂,在 Android 編程中宦搬,它使用的是 UI 線程。

現(xiàn)在劫拗,你應該可以很靈活地使用各種 dispatcher 了间校。

協(xié)程構造器(Coroutine Builders

現(xiàn)在,你可以輕松地切換線程了页慷。接下來憔足,我們學習一下如何啟動一個新的協(xié)程:當然要靠協(xié)程構造器了。

根據(jù)實際情況酒繁,我們可以選擇使用不同的協(xié)程構造器滓彰,當然我們也可以創(chuàng)建自定義的協(xié)程構造器。不過通常情況下州袒,協(xié)程庫提供的已經(jīng)滿足我們的使用了揭绑。具體如下:

runBlocking

這個協(xié)程構造器會阻塞當前線程,直到協(xié)程內(nèi)的所有任務執(zhí)行完畢稳析。這好像違背了我們使用協(xié)程的初衷洗做,所以什么場景下會用到它呢?

runBlocking 對于測試可中斷的方法非常有用彰居。在測試的時候诚纸,將可中斷的方法運行在 runBlocking 構建的協(xié)程內(nèi)部,這樣就可以保證陈惰,在這些可中斷的方法返回結果前當前測試線程不會結束畦徘,這樣,我們就可以校驗測試結果了抬闯。

fun testSuspendingFunction() = runBlocking {
    val res = suspendingTask1()
    assertEquals(0, res)
}

但是井辆,除了這個場景外,你也許不會用到 runBlocking 了溶握。

launch

這個協(xié)程構造器很重要杯缺,因為它可以很輕易地創(chuàng)建一個協(xié)程,你可能會經(jīng)常用到它睡榆。和 runBlocking 相反的是萍肆,它不會阻塞當前線程(前提是我們使用了合適的 dispatcher)袍榆。

這個協(xié)程構造器通常需要一個作用域(scope),關于作用域的概念后面會講到塘揣,我們暫時使用全局作用域(GlobalScope):

GlobalScope.launch(Dispatchers.Main) {
    ...
}

launch 方法會返回一個 Job包雀,Job 繼承了協(xié)程上下文(CoroutineContext)。

Job 提供了很多有用的方法亲铡。需要明確的是:一個 Job 可以有一個父 Job才写,父 Job 可以控制子 Job。下面介紹一下 Job 的方法:

job.join

這個方法可以中斷與當前 Job 關聯(lián)的協(xié)程奖蔓,直到所有子 Job 執(zhí)行完成赞草。協(xié)程內(nèi)的所有可中斷的方法與當前 Job 相關聯(lián),直到子 Job 全部執(zhí)行完成锭硼,與當前 Job 關聯(lián)的協(xié)程才能繼續(xù)執(zhí)行房资。

val job = GlobalScope.launch(Dispatchers.Main) {

    doCoroutineTask()

    val res1 = suspendingTask1()
    val res2 = suspendingTask2()

    process(res1, res2)

}

job.join()

job.join() 是一個可中斷的方法蜕劝,所以它應該在協(xié)程內(nèi)部被調(diào)用檀头。

job.cancel

這個方法可以取消所有與其關聯(lián)的子 Job,假如 suspendingTask1() 正在執(zhí)行的時候 Job 調(diào)用了 cancel() 方法岖沛,這時候暑始,res1 不會再被返回,而且 suspendingTask2() 也不會再執(zhí)行婴削。

val job = GlobalScope.launch(Dispatchers.Main) {

    doCoroutineTask()

    val res1 = suspendingTask1()
    val res2 = suspendingTask2()

    process(res1, res2)

}

job.cancel()

job.cancel() 是一個普通方法廊镜,所以它不必運行在協(xié)程內(nèi)部。

async

這個協(xié)程構造器將會解決我們在剛開始演示示例的時候提到的一些難題唉俗。

async 允許并行地運行多個子線程任務嗤朴,它不是一個可中斷方法,所以當調(diào)用 async 啟動子協(xié)程的同時虫溜,后面的代碼也會立即執(zhí)行雹姊。async 通常需要運行在另外一個協(xié)程內(nèi)部,它會返回一個特殊的 Job衡楞,叫作 Deferred吱雏。

Deferred 有一個新的方法叫做 await(),它是一個可中斷的方法瘾境,當我們需要獲取 async 的結果時歧杏,需要調(diào)用 await() 方法等待結果。調(diào)用 await() 方法后迷守,會中斷當前協(xié)程犬绒,直到其返回結果。

在下面的示例中兑凿,第二個和第三個請求需要依賴第一個請求的結果凯力,請求好友列表和推薦好友列表本來可以并行請求的眨业,如果都使用 withContext,顯然會浪費時間:

GlobalScope.launch(Dispatchers.Main) {

    val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
    val currentFriends = withContext(Dispatchers.IO) { userService.requestCurrentFriends(user) }
    val suggestedFriends = withContext(Dispatchers.IO) { userService.requestSuggestedFriends(user) }

    val finalUser = user.copy(friends = currentFriends + suggestedFriends)
}

假如每個請求耗時 2 秒沮协,總共需要使用 6 秒龄捡。如果我們使用 async 替代呢:

GlobalScope.launch(Dispatchers.Main) {

    val user = withContext(Dispatchers.IO) { userService.doLogin(username, password) }
    val currentFriends = async(Dispatchers.IO) { userService.requestCurrentFriends(user) }
    val suggestedFriends = async(Dispatchers.IO) { userService.requestSuggestedFriends(user) }

    val finalUser = user.copy(friends = currentFriends.await() + suggestedFriends.await())

}

這時,第二個和第三個請求會并行運行慷暂,所以總耗時將會減少到 4 秒聘殖。

作用域(Scope

到目前為止,我們使用簡單的方式輕松地實現(xiàn)了復雜的操作行瑞。但是奸腺,仍有一個問題未解決。

假如我們要使用 RecyclerView 顯示朋友列表血久,當請求仍在進行的時候突照,客戶關閉了 activity,此時 activity 處于 isFinishing 的狀態(tài)氧吐,任何更新 UI 的操作都會導致 App 崩潰讹蘑。

我們怎么處理這種場景呢?當然是使用作用域(scope)了筑舅。先來看看都有哪些作用域:

Global scope

它是一個全局的作用域座慰,如果協(xié)程的運行周期和 App 的生命周期一樣長的話,創(chuàng)建協(xié)程的時候可以使用它翠拣。所以它不應該和任何可以被銷毀的組件綁定使用版仔。

它的使用方式是這樣的:

GlobalScope.launch(Dispatchers.Main) {
    ...
}

當你使用它的時候,要再三確定误墓,要創(chuàng)建的協(xié)程是否需要伴隨 App 整個生命周期運行蛮粮,并且這個協(xié)程沒有和界面、組件等綁定谜慌。

自定義協(xié)程作用域

任何類都可以繼承 CoroutineScope 作為一個作用域然想。你需要做的唯一一件事就是重寫 coroutineContext 這個屬性。

在此之前畦娄,你需要明確兩個重要的概念 dispatcherJob又沾。

不知道你是否還記得,一個上下文(context)可以是多個上下文的組合熙卡。組合的上下文需要是不同的類型杖刷。所以,你需要做兩件事情:

  • 一個 dispatcher: 用于指定協(xié)程默認使用的 dispatcher驳癌;
  • 一個 job: 用于在任何需要的時候取消協(xié)程滑燃;
class MainActivity : AppCompatActivity(), CoroutineScope {

    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    private lateinit var job: Job

}

操作符號 + 用于組合上下文。如果兩種不同類型的上下文相組合颓鲜,會生成一個組合的上下文(CombinedContext)表窘,這個新的上下文會同時擁有被組合上下文的特性典予。

如果兩個相同類型的上下文相組合,新的上下文等同于第二個上下文乐严。即 Dispatchers.Main + Dispatchers.IO == Dispatchers.IO瘤袖。

我們可以使用延遲初始化(lateinit)的方式創(chuàng)建一個 Job。這樣我們就可以在 onCreate() 方法中初始化它昂验,在 onDestroy() 方法中取消它捂敌。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    job = Job()
    ...
}

override fun onDestroy() {
    job.cancel()
    super.onDestroy()
}

這樣以來,使用協(xié)程就方便多了既琴。我們只管創(chuàng)建協(xié)程占婉,而不用關心使用的上下文。因為我們已經(jīng)在自定義的作用域里面聲明了上下文甫恩,也就是包含了 main dispatcher 的那個上下文:

launch {
    ...
}

如果你的所有 activity 都需要使用協(xié)程逆济,將上述代碼提取到一個父類中是很有必要的。

附錄1 - 回調(diào)方式轉為協(xié)程

如果你已經(jīng)考慮將協(xié)程用于現(xiàn)有的項目磺箕,你可能會考慮怎么將現(xiàn)有的回調(diào)風格的代碼轉為協(xié)程:

suspend fun suspendAsyncLogin(username: String, password: String): User =
    suspendCancellableCoroutine { continuation ->
        userService.doLoginAsync(username, password) { user ->
            continuation.resume(user)
        }
    }

suspendCancellableCoroutine() 這個方法返回一個 continuation 對象奖慌,continuation 可以用于返回回調(diào)的結果。只要調(diào)用 continuation.resume() 方法滞磺,這個回調(diào)結果就可以作為這個可中斷方法的結果返回給協(xié)程升薯。

附錄2 - 協(xié)程和 RxJava

每次提到協(xié)程都會有人問起莱褒,協(xié)程可以替代 RxJava 嗎击困?簡單地回答就是:不可以。

客觀地來說广凸,根據(jù)情況而定:

  1. 如果你使用 RxJava 只是用來從主線程切換到子線程阅茶。你也看到了,協(xié)程可以輕松地實現(xiàn)這一點谅海。這種情況下脸哀,完全可以替代 RxJava
  2. 如果你使用 RxJava 用來流式編程扭吁,合并流撞蜂、轉換流等。RxJava 依然更有優(yōu)勢侥袜。協(xié)程中有一個 Channels 的概念蝌诡,可以替代 RxJava 實現(xiàn)一些簡單的場景,但是通常情況下枫吧,你可能更傾向于使用 RxJava 的流式編程浦旱。

值得一提的是,這里有一個開源庫九杂,可以在協(xié)程中使用 RxJava颁湖,你可能會感興趣宣蠕。

總結

協(xié)程為我們打開了一個充滿無限可能性、更簡單實現(xiàn)異步編程的世界甥捺。在此之前抢蚀,這是不可想象的。

強烈推薦把協(xié)程用于你現(xiàn)有的項目當中镰禾。如果你想查看完整的示例代碼思币,點擊這里

趕快開啟你的協(xié)程之旅吧羡微!

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谷饿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子妈倔,更是在濱河造成了極大的恐慌博投,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盯蝴,死亡現(xiàn)場離奇詭異毅哗,居然都是意外死亡,警方通過查閱死者的電腦和手機捧挺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門虑绵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闽烙,你說我怎么就攤上這事翅睛。” “怎么了黑竞?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵捕发,是天一觀的道長。 經(jīng)常有香客問我很魂,道長扎酷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任遏匆,我火速辦了婚禮法挨,結果婚禮上,老公的妹妹穿的比我還像新娘幅聘。我一直安慰自己凡纳,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布喊暖。 她就那樣靜靜地躺著惫企,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狞尔,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天丛版,我揣著相機與錄音,去河邊找鬼偏序。 笑死页畦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的研儒。 我是一名探鬼主播豫缨,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼端朵!你這毒婦竟也來了好芭?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤冲呢,失蹤者是張志新(化名)和其女友劉穎舍败,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體敬拓,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡邻薯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了乘凸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厕诡。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖营勤,靈堂內(nèi)的尸體忽然破棺而出灵嫌,到底是詐尸還是另有隱情,我是刑警寧澤冀偶,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布醒第,位于F島的核電站,受9級特大地震影響进鸠,放射性物質發(fā)生泄漏。R本人自食惡果不足惜形病,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一客年、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧漠吻,春花似錦量瓜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春烫饼,著一層夾襖步出監(jiān)牢的瞬間猎塞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工杠纵, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荠耽,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓比藻,卻偏偏與公主長得像铝量,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子银亲,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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