Android kotlin協(xié)程入門(二):kotlin協(xié)程的關(guān)鍵知識(shí)點(diǎn)初步講解

由于文章涉及到的只是點(diǎn)比較多胞谭、內(nèi)容可能過長(zhǎng)垃杖,可以根據(jù)自己的能力水平和熟悉程度分階段跳著看。如有講述的不正確的地方勞煩各位私信給筆者丈屹,萬分感謝调俘。

kotlin協(xié)程的關(guān)鍵知識(shí)點(diǎn)

上一本章節(jié)《Android kotlin協(xié)程入門實(shí)戰(zhàn)(一):kotlin協(xié)程的基礎(chǔ)用法解讀》末尾我們提到,將在本章節(jié)中對(duì)以下知識(shí)點(diǎn)做初步講解旺垒,包含上文提到的launchasync函數(shù)中的3個(gè)參數(shù)作用彩库。清單如下:

  1. 協(xié)程調(diào)度器CoroutineDispatcher
  2. 協(xié)程下上文CoroutineContext作用
  3. 協(xié)程啟動(dòng)模式CoroutineStart
  4. 協(xié)程作用域CoroutineScope
  5. 掛起函數(shù)以及suspend關(guān)鍵字的作用

當(dāng)然還有一些其他的知識(shí)點(diǎn)也是很重要的,比如:CoroutineExceptionHandler先蒋、Continuation骇钦、SchedulerContinuationInterceptor等竞漾。但是確實(shí)涉及到的東西比較多眯搭,如果都展開的話,可能再寫幾個(gè)篇幅都沒有辦法講完业岁。上面這些是筆者認(rèn)為掌握了這些知識(shí)點(diǎn)以后鳞仙,基本可以開始著手項(xiàng)目實(shí)戰(zhàn)了。我們后面在實(shí)戰(zhàn)的過程中笔时,邊寫邊講解棍好。

協(xié)程調(diào)度器

上文我們提到一個(gè)協(xié)程調(diào)度器CoroutineDispatcher的概念,調(diào)度器又是一個(gè)什么神奇的東西。在這里我們對(duì)調(diào)度器不做過多深入的解釋借笙,這可是協(xié)程的三大件之一扒怖,后面我們會(huì)有專門的篇幅做深入講解。為了方便我們把協(xié)程調(diào)度器簡(jiǎn)稱為調(diào)度器业稼,那接下來我們就看看什么是調(diào)度器盗痒。偷個(gè)懶,引用一下官方的原話:

  • 調(diào)度器它確定了相關(guān)的協(xié)程在哪個(gè)線程或哪些線程上執(zhí)行盼忌。協(xié)程調(diào)度器可以將協(xié)程限制在一個(gè)特定的線程執(zhí)行,或?qū)⑺峙傻揭粋€(gè)線程池掂墓,亦或是讓它不受限地運(yùn)行谦纱。

對(duì)于調(diào)度器的實(shí)現(xiàn)機(jī)制我們已經(jīng)非常清楚了,官方框架中預(yù)置了4個(gè)調(diào)度器君编,我們可以通過Dispatchers對(duì)象直接訪問它們:

public actual object Dispatchers {
    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    @JvmStatic
    public actual val Main: MainCoroutineDispatcher
        get() = MainDispatcherLoader.dispatcher
    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
復(fù)制代碼
  • Default:默認(rèn)調(diào)度器跨嘉,CPU密集型任務(wù)調(diào)度器,適合處理后臺(tái)計(jì)算吃嘿。通常處理一些單純的計(jì)算任務(wù)祠乃,或者執(zhí)行時(shí)間較短任務(wù)。比如:Json的解析兑燥,數(shù)據(jù)計(jì)算等
  • IO:IO調(diào)度器亮瓷,,IO密集型任務(wù)調(diào)度器降瞳,適合執(zhí)行IO相關(guān)操作嘱支。比如:網(wǎng)絡(luò)處理,數(shù)據(jù)庫操作挣饥,文件操作等
  • Main:UI調(diào)度器除师, 即在主線程上執(zhí)行,通常用于UI交互扔枫,刷新等
  • Unconfined:非受限調(diào)度器汛聚,又或者稱為“無所謂”調(diào)度器,不要求協(xié)程執(zhí)行在特定線程上短荐。

比如上面我們通過launch啟動(dòng)的時(shí)候倚舀,因?yàn)槲覀儧]有傳入?yún)?shù),所有實(shí)際上它使用的是默認(rèn)調(diào)度器Dispatchers.Default

GlobalScope.launch{
    Log.d("launch", "啟動(dòng)一個(gè)協(xié)程")
}
//等同于
GlobalScope.launch(Dispatchers.Default){
    Log.d("launch", "啟動(dòng)一個(gè)協(xié)程")
}
復(fù)制代碼

Dispatchers.IODispatchers.Main就都很好理解了忍宋。這是我們以后在Android開發(fā)過程中瞄桨,打交道最多的2個(gè)調(diào)度器。比如后臺(tái)數(shù)據(jù)上傳讶踪,我們就可以使用Dispatchers.IO調(diào)度器芯侥。刷新界面我們就使用Dispatchers.Main調(diào)度器。為方便使用官方在Android協(xié)程框架庫中,已經(jīng)為我們定義好了幾個(gè)供我們開發(fā)使用柱查,如:MainScope廓俭、lifecycleScopeviewModelScope唉工。它們都是使用的Dispatchers.Main研乒,這些后續(xù)我們都將會(huì)使用到。

根據(jù)我們上面使用的方法淋硝,我們好像只有在啟動(dòng)協(xié)程的時(shí)候雹熬,才能指定具體使用那個(gè)Dispatchers調(diào)度器。如果我要是想中途切換線程怎么辦谣膳,比如:

  • 現(xiàn)在我們需要通過網(wǎng)絡(luò)請(qǐng)求獲取到數(shù)據(jù)的時(shí)候填充到我們的布局當(dāng)中竿报,但是網(wǎng)絡(luò)處理在IO線程上,而刷新UI是在主線程上继谚,那我們應(yīng)該怎么辦烈菌。

莫慌,莫慌花履,萬事萬物總有解決的辦法芽世。官方為我們提供了一個(gè)withContext頂級(jí)函數(shù),使用withContext函數(shù)來改變協(xié)程的上下文诡壁,而仍然駐留在相同的協(xié)程中济瓢,同時(shí)withContext還攜帶有一個(gè)泛型T返回值。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
 //......
}
復(fù)制代碼

呀妹卿,這一看withContext這個(gè)東西好像很符合我們的需求嘛葬荷,我們可以先使用launch(Dispatchers.Main)啟動(dòng)協(xié)程,然后再通過withContext(Dispatchers.IO)調(diào)度到IO線程上去做網(wǎng)絡(luò)請(qǐng)求纽帖,把得到的結(jié)果返回,這樣我們就解決了我們上面的問題了宠漩。

GlobalScope.launch(Dispatchers.Main) {
    val result = withContext(Dispatchers.IO) {
        //網(wǎng)絡(luò)請(qǐng)求...
        "請(qǐng)求結(jié)果"
    }
    btn.text = result
}
復(fù)制代碼

是不是很簡(jiǎn)單!0弥薄扒吁! 麻麻再也不會(huì)說我的handler滿飛了,也不用走那萬惡的回調(diào)地獄了室囊。我想怎么切就怎么切雕崩,想去走個(gè)線程就去哪個(gè)線程。邏輯都按著順序一步一步走融撞,而且代碼都是這么的絲滑盼铁。還要什么自行車,額.錯(cuò)了尝偎,還要什么handler饶火,管他回調(diào)不回調(diào)鹏控。

協(xié)程上下文

CoroutineContext即協(xié)程上下文。它是一個(gè)包含了用戶定義的一些各種不同元素的Element對(duì)象集合。其中主要元素是Job、協(xié)程調(diào)度器CoroutineDispatcher覆旱、還有包含協(xié)程異常CoroutineExceptionHandler、攔截器ContinuationInterceptor砸逊、協(xié)程名CoroutineName等。這些數(shù)據(jù)都是和協(xié)程密切相關(guān)的,每一個(gè)Element都一個(gè)唯一key。

public interface CoroutineContext {
    public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?

    public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R

    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else context.fold(this) { ...}

    public fun minusKey(key: Key<*>): CoroutineContext

    //注意這里找筝,這個(gè)key很關(guān)鍵
    public interface Key <E : CoroutineContext.Element>

     public interface Element : CoroutineContext {
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
         }
}
復(fù)制代碼

我們可以看到ElementCoroutineContext的內(nèi)部接口,同時(shí)它又實(shí)現(xiàn)了CoroutineContext接口慷吊,這么設(shè)計(jì)的原因是為了保證Element中一定只能存放的Element它自己袖裕,而不能存放其他類型的數(shù)據(jù)CoroutineContext內(nèi)還有一個(gè)內(nèi)部接口Key,同時(shí)它又是Element的一個(gè)屬性罢浇,這個(gè)屬性很重要陆赋,我們先在這里插個(gè)眼沐祷,待會(huì)再講解這個(gè)屬性的作用嚷闭。

那我們上面提到JobCoroutineDispatcher赖临、CoroutineExceptionHandler胞锰、ContinuationInterceptorCoroutineName等為什么又可以存放到CoroutineContext中呢兢榨。我們接著往下看看它們各自的實(shí)現(xiàn):

Job

public interface Job : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<Job> {
        //省略...
    }
}
復(fù)制代碼

CoroutineDispatcher

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
      public companion object Key : AbstractCoroutineContextKey<ContinuationInterceptor, CoroutineDispatcher>(
        ContinuationInterceptor,
        { it as? CoroutineDispatcher })
}
復(fù)制代碼

CoroutineExceptionHandler

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
}
復(fù)制代碼

ContinuationInterceptor

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}
復(fù)制代碼

CoroutineName

public data class CoroutineName(
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
    public companion object Key : CoroutineContext.Key<CoroutineName>
}
復(fù)制代碼

現(xiàn)在要開始要集中注意力了嗅榕。我們可以看到他們都是實(shí)現(xiàn)了Element接口,同時(shí)都有個(gè)CoroutineContext.Key類型的伴生對(duì)象key,這個(gè)屬性的作用是什么呢吵聪。那我們就得回過頭來看看CoroutineContext接口的幾個(gè)方法了凌那。

public operator fun <E : CoroutineContext.Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else context.fold(this) { ...}

public fun minusKey(key: Key<*>): CoroutineContext
復(fù)制代碼

我們先從plus方法說起,plus有個(gè)關(guān)鍵字operator表示這是一個(gè)運(yùn)算符重載的方法吟逝,類似List.plus的運(yùn)算符帽蝶,可以通過+號(hào)來返回一個(gè)包含原始集合和第二個(gè)操作數(shù)中的元素的結(jié)果。同理CoroutineContext中是通過plus來返回一個(gè)由原始的Element集合和通過+號(hào)引入的Element產(chǎn)生新的Element集合块攒。

get方法励稳,顧名思義〈丫可以通過 key 來獲取一個(gè)Element

fold方法它和集合中的fold是一樣的驹尼,用來遍歷當(dāng)前協(xié)程上下文中的Element集合。

minusKey方法plus作用相反庞呕,它相當(dāng)于是做減法,是用來取出除key以外的當(dāng)前協(xié)程上下文其他Element新翎,返回的就是不包含key的協(xié)程上下文。

現(xiàn)在我們就知道為什么我們之前說Element中的key這個(gè)屬性很重要了吧。因?yàn)槲覀兙褪峭ㄟ^它從協(xié)程上下文中獲取我們想要的Element料祠,同時(shí)也解釋為什么Job骆捧、CoroutineDispatcherCoroutineExceptionHandler髓绽、ContinuationInterceptor敛苇、CoroutineName等等,這些Element都有需要有一個(gè)CoroutineContext.Key類型的伴生對(duì)象key顺呕。我們寫個(gè)測(cè)試方法: 如:

 private fun testCoroutineContext(){
     val coroutineContext1 = Job() + CoroutineName("這是第一個(gè)上下文")
     Log.d("coroutineContext1", "$coroutineContext1")
     val  coroutineContext2 = coroutineContext1 + Dispatchers.Default + CoroutineName("這是第二個(gè)上下文")
     Log.d("coroutineContext2", "$coroutineContext2")
     val coroutineContext3 = coroutineContext2 + Dispatchers.Main + CoroutineName("這是第三個(gè)上下文")
     Log.d("coroutineContext3", "$coroutineContext3")
 }
復(fù)制代碼
D/coroutineContext1: [JobImpl{Active}@21a6a21, CoroutineName(這是第一個(gè)上下文)]
D/coroutineContext2: [JobImpl{Active}@21a6a21, CoroutineName(這是第二個(gè)上下文), Dispatchers.Default]
D/coroutineContext3: [JobImpl{Active}@21a6a21, CoroutineName(這是第三個(gè)上下文), Dispatchers.Main]
復(fù)制代碼

我們通過對(duì)比日志輸出信息可以看到枫攀,通過+號(hào)我們可以把多個(gè)Element整合到一個(gè)集合中,同時(shí)我們也發(fā)現(xiàn):

  • 三個(gè)上下文中的Job是同一個(gè)對(duì)象株茶。
  • 第二個(gè)上下文在第一個(gè)的基礎(chǔ)上增加了一個(gè)新的CoroutineName,新增的CoroutineName替換了第一個(gè)上下文中的CoroutineName来涨。
  • 第三個(gè)上下文在第二個(gè)的基礎(chǔ)上又增加了一個(gè)新的CoroutineNameDispatchers,同時(shí)他們也替換了第二個(gè)上下文中的CoroutineNameDispatchers

但是因?yàn)檫@個(gè)+運(yùn)算符是不對(duì)稱的启盛,所以在我們實(shí)際的運(yùn)用過程中蹦掐,通過+增加Element的時(shí)候一定要注意它們結(jié)合的順序。那么現(xiàn)在關(guān)于協(xié)程上下文的內(nèi)容就講到這里僵闯,我們點(diǎn)到為止卧抗,后面在深入理解階段在細(xì)講這些東西運(yùn)行的原理細(xì)節(jié)。

協(xié)程啟動(dòng)模式

CoroutineStart協(xié)程啟動(dòng)模式鳖粟,是啟動(dòng)協(xié)程時(shí)需要傳入的第二個(gè)參數(shù)社裆。協(xié)程啟動(dòng)有4種:

  • DEFAULT 默認(rèn)啟動(dòng)模式,我們可以稱之為餓漢啟動(dòng)模式向图,因?yàn)閰f(xié)程創(chuàng)建后立即開始調(diào)度泳秀,雖然是立即調(diào)度,單不是立即執(zhí)行榄攀,有可能在執(zhí)行前被取消嗜傅。

  • LAZY 懶漢啟動(dòng)模式,啟動(dòng)后并不會(huì)有任何調(diào)度行為檩赢,直到我們需要它執(zhí)行的時(shí)候才會(huì)產(chǎn)生調(diào)度吕嘀。也就是說只有我們主動(dòng)的調(diào)用Jobstartjoin或者await等函數(shù)時(shí)才會(huì)開始調(diào)度漠畜。

  • ATOMIC 一樣也是在協(xié)程創(chuàng)建后立即開始調(diào)度币他,但是它和DEFAULT模式有一點(diǎn)不一樣,通過ATOMIC模式啟動(dòng)的協(xié)程執(zhí)行到第一個(gè)掛起點(diǎn)之前是不響應(yīng)cancel 取消操作的憔狞,ATOMIC一定要涉及到協(xié)程掛起后cancel 取消操作的時(shí)候才有意義蝴悉。

  • UNDISPATCHED 協(xié)程在這種模式下會(huì)直接開始在當(dāng)前線程下執(zhí)行,直到運(yùn)行到第一個(gè)掛起點(diǎn)瘾敢。這聽起來有點(diǎn)像 ATOMIC拍冠,不同之處在于UNDISPATCHED是不經(jīng)過任何調(diào)度器就開始執(zhí)行的尿这。當(dāng)然遇到掛起點(diǎn)之后的執(zhí)行,將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器庆杜。

我們可以通過一個(gè)小例子的來看看這幾個(gè)啟動(dòng)模式的實(shí)際情況:

private fun testCoroutineStart(){
    val defaultJob = GlobalScope.launch{
        Log.d("defaultJob", "CoroutineStart.DEFAULT")
    }
    defaultJob.cancel()
    val lazyJob = GlobalScope.launch(start = CoroutineStart.LAZY){
        Log.d("lazyJob", "CoroutineStart.LAZY")
    }
    val atomicJob = GlobalScope.launch(start = CoroutineStart.ATOMIC){
        Log.d("atomicJob", "CoroutineStart.ATOMIC掛起前")
        delay(100)
        Log.d("atomicJob", "CoroutineStart.ATOMIC掛起后")
    }
    atomicJob.cancel()
    val undispatchedJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED){
        Log.d("undispatchedJob", "CoroutineStart.UNDISPATCHED掛起前")
        delay(100)
        Log.d("atomicJob", "CoroutineStart.UNDISPATCHED掛起后")
    }
    undispatchedJob.cancel()
}
復(fù)制代碼

每個(gè)模式我們分別啟動(dòng)一個(gè)一次射众,DEFAULT模式啟動(dòng)時(shí),我們接著調(diào)用了cancel取消協(xié)程晃财,ATOMIC模式啟動(dòng)時(shí),我們?cè)诶锩嬖黾恿艘粋€(gè)掛起點(diǎn)delay掛起函數(shù)叨橱,來區(qū)分ATOMIC啟動(dòng)時(shí)的掛起前后執(zhí)行情況,同樣的UNDISPATCHED模式啟動(dòng)時(shí)断盛,我們也調(diào)用了cancel取消協(xié)程罗洗,我們看實(shí)際的日志輸出情況:

D/defaultJob: CoroutineStart.DEFAULT
D/atomicJob: CoroutineStart.ATOMIC掛起前
D/undispatchedJob: CoroutineStart.UNDISPATCHED掛起前
復(fù)制代碼

或者

D/undispatchedJob: CoroutineStart.UNDISPATCHED掛起前
D/atomicJob: CoroutineStart.ATOMIC掛起前
復(fù)制代碼

為什么會(huì)出現(xiàn)2種情況。我們上面提到過DEFAULT模式協(xié)程創(chuàng)建后立即開始調(diào)度钢猛,但不是立即執(zhí)行伙菜,所有它有可能會(huì)被cancel取消,導(dǎo)致沒有輸出defaultJob這條日志命迈。

同樣的ATOMIC模式啟動(dòng)的時(shí)候也接著調(diào)用了cancel取消協(xié)程贩绕,但是因?yàn)闆]有遇到掛起點(diǎn),所以掛起前的日志輸出了壶愤,但是掛起后的日志沒有輸出淑倾。

UNDISPATCHED模式啟動(dòng)的時(shí)候也接著調(diào)用了cancel取消協(xié)程,同樣的因?yàn)闆]有遇到掛起點(diǎn)所以輸出了UNDISPATCHED掛起前公你,但是因?yàn)?code>UNDISPATCHED是立即執(zhí)行的踊淳,所以他的日志UNDISPATCHED掛起前輸出在ATOMIC掛起前的前面假瞬。

接著我們?cè)谘a(bǔ)充一下關(guān)于UNDISPATCHED模式陕靠。我們上面有提到當(dāng)以UNDISPATCHED模式啟動(dòng)時(shí),遇到掛起點(diǎn)之后的執(zhí)行脱茉,將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器剪芥。這句話我們又要怎么理解呢。我們還是以一個(gè)例子來認(rèn)識(shí)解釋UNDISPATCHED模式琴许,比如:

private fun testUnDispatched(){
    GlobalScope.launch(Dispatchers.Main){
       val job = launch(Dispatchers.IO) {
           Log.d("${Thread.currentThread().name}線程", "-> 掛起前")
           delay(100)
           Log.d("${Thread.currentThread().name}線程", "-> 掛起后")
       }
       Log.d("${Thread.currentThread().name}線程", "-> join前")
       job.join()
       Log.d("${Thread.currentThread().name}線程", "-> join后")
   }
}
復(fù)制代碼

那我們將會(huì)看到如下輸出,掛起前后都在一個(gè)worker-1線程里面執(zhí)行:

D/main線程: -> join前
D/DefaultDispatcher-worker-1線程: -> 掛起前
D/DefaultDispatcher-worker-1線程: -> 掛起后
D/main線程: -> join后
復(fù)制代碼

現(xiàn)在我們?cè)谏宰餍薷乃胺荆覀冊(cè)谧訁f(xié)程launch的時(shí)候使用UNDISPATCHED模式啟動(dòng):

 private fun testUnDispatched(){
     GlobalScope.launch(Dispatchers.Main){
        val job = launch(Dispatchers.IO,start = CoroutineStart.UNDISPATCHED) {
            Log.d("${Thread.currentThread().name}線程", "-> 掛起前")
            delay(100)
            Log.d("${Thread.currentThread().name}線程", "-> 掛起后")
        }
        Log.d("${Thread.currentThread().name}線程", "-> join前")
        job.join()
        Log.d("${Thread.currentThread().name}線程", "-> join后")
    }
 }
復(fù)制代碼

那我們將會(huì)看到如下輸出:

D/main線程: -> 掛起前
D/main線程: -> join前
D/DefaultDispatcher-worker-1線程: -> 掛起后
D/main線程: -> join后
復(fù)制代碼

我們看到當(dāng)以UNDISPATCHED模式即使我們指定了協(xié)程調(diào)度器Dispatchers.IO掛起前還是在main線程里執(zhí)行榜田,但是掛起后是在worker-1線程里面執(zhí)行益兄,這是因?yàn)楫?dāng)以UNDISPATCHED啟動(dòng)時(shí),協(xié)程在這種模式下會(huì)直接開始在當(dāng)前線程下執(zhí)行,直到第一個(gè)掛起點(diǎn)箭券。遇到掛起點(diǎn)之后的執(zhí)行净捅,將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器,即join處恢復(fù)執(zhí)行時(shí)辩块,因?yàn)樗诘膮f(xié)程有調(diào)度器蛔六,所以后面的執(zhí)行將會(huì)在調(diào)度器對(duì)應(yīng)的線程上執(zhí)行荆永。

我們?cè)俑囊幌拢炎訁f(xié)程在launch的時(shí)候使用UNDISPATCHED模式啟動(dòng)国章,去掉Dispatchers.IO調(diào)度器具钥,那又會(huì)出現(xiàn)什么情況呢

 private fun testUnDispatched(){
     GlobalScope.launch(Dispatchers.Main){
        val job = launch(start = CoroutineStart.UNDISPATCHED) {
            Log.d("${Thread.currentThread().name}線程", "-> 掛起前")
            delay(100)
            Log.d("${Thread.currentThread().name}線程", "-> 掛起后")
        }
        Log.d("${Thread.currentThread().name}線程", "-> join前")
        job.join()
        Log.d("${Thread.currentThread().name}線程", "-> join后")
    }
 }
復(fù)制代碼
D/main線程: -> 掛起前
D/main線程: -> join前
D/main線程: -> 掛起后
D/main線程: -> join后
復(fù)制代碼

我們發(fā)現(xiàn)它們都在一個(gè)線程里面執(zhí)行了。這是因?yàn)楫?dāng)通過UNDISPATCHED啟動(dòng)后遇到掛起液兽,join處恢復(fù)執(zhí)行時(shí)骂删,如果所在的協(xié)程沒有指定調(diào)度器,那么就會(huì)在join處恢復(fù)執(zhí)行的線程里執(zhí)行四啰,即掛起后是在父協(xié)程(Dispatchers.Main線程里面執(zhí)行桃漾,而最后join后這條日志的輸出調(diào)度取決于這個(gè)最外層的協(xié)程的調(diào)度規(guī)則。

現(xiàn)在我們可以總結(jié)一下拟逮,當(dāng)以UNDISPATCHED啟動(dòng)時(shí):

  • 無論我們是否指定協(xié)程調(diào)度器撬统,掛起前的執(zhí)行都是在當(dāng)前線程下執(zhí)行。

  • 如果所在的協(xié)程沒有指定調(diào)度器敦迄,那么就會(huì)在join處恢復(fù)執(zhí)行的線程里執(zhí)行恋追,即我們上述案例中的掛起后的執(zhí)行是在main線程中執(zhí)行。

  • 當(dāng)我們指定了協(xié)程調(diào)度器時(shí)罚屋,遇到掛起點(diǎn)之后的執(zhí)行將取決于掛起點(diǎn)本身的邏輯和協(xié)程上下文中的調(diào)度器苦囱。即join處恢復(fù)執(zhí)行時(shí),因?yàn)樗诘膮f(xié)程有調(diào)度器脾猛,所以后面的執(zhí)行將會(huì)在調(diào)度器對(duì)應(yīng)的線程上執(zhí)行撕彤。

同樣的我們點(diǎn)到為止,關(guān)于啟動(dòng)模式的的相關(guān)內(nèi)容我們就現(xiàn)講到這里猛拴。

協(xié)程作用域

協(xié)程作用域CoroutineScope為協(xié)程定義作用范圍羹铅,每個(gè)協(xié)程生成器launchasync等都是CoroutineScope的擴(kuò)展愉昆,并繼承了它的coroutineContext自動(dòng)傳播其所有Element和取消职员。協(xié)程作用域本質(zhì)是一個(gè)接口,不建議手工實(shí)現(xiàn)該接口,而應(yīng)該首選委托實(shí)現(xiàn)跛溉。下面我們列出了部分CoroutineScope相關(guān)定義:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
    ContextScope(coroutineContext + context)

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())
復(fù)制代碼

我們可以看到CoroutineScope也重載了plus方法焊切,通過+號(hào)來新增或者修改我們CoroutineContext協(xié)程上下文中的Element。同時(shí)官方也為我們定義好了 MainScopeGlobalScope2個(gè)頂級(jí)作用域芳室。GlobalScope我們已經(jīng)很熟了专肪,前面的案例都是通過它來實(shí)現(xiàn)的。

MainScope我們可以看到它的上下文是通過SupervisorJobDispatchers.Main組合的堪侯,說明它是一個(gè)在主線程執(zhí)行的協(xié)程作用域嚎尤,我們?cè)诤罄m(xù)的Android實(shí)戰(zhàn)開發(fā)中,會(huì)結(jié)合Activity抖格、Fragment诺苹,dialog等使用它咕晋。這里不再繼續(xù)往下擴(kuò)展。

至于SupervisorJob分析它之前收奔,我們得先說一下協(xié)程作用域的分類掌呜。我們之前提到過父協(xié)程和子協(xié)程的概念,既然有父協(xié)程和子協(xié)程坪哄,那么必然也有父協(xié)程作用域和子父協(xié)程作用域质蕉。不過我們不是這么稱呼,因?yàn)樗麄儾粌H僅是父與子的概念翩肌。協(xié)程作用域分為三種:

  • 頂級(jí)作用域 --> 沒有父協(xié)程的協(xié)程所在的作用域稱之為頂級(jí)作用域模暗。

  • 協(xié)同作用域 --> 在協(xié)程中啟動(dòng)一個(gè)協(xié)程,新協(xié)程為所在協(xié)程的子協(xié)程念祭。子協(xié)程所在的作用域默認(rèn)為協(xié)同作用域。此時(shí)子協(xié)程拋出未捕獲的異常時(shí)粱坤,會(huì)將異常傳遞給父協(xié)程處理隶糕,如果父協(xié)程被取消,則所有子協(xié)程同時(shí)也會(huì)被取消站玄。

  • 主從作用域 官方稱之為監(jiān)督作用域枚驻。與協(xié)同作用域一致,區(qū)別在于該作用域下的協(xié)程取消操作的單向傳播性株旷,子協(xié)程的異常不會(huì)導(dǎo)致其它子協(xié)程取消再登。但是如果父協(xié)程被取消,則所有子協(xié)程同時(shí)也會(huì)被取消晾剖。

同時(shí)補(bǔ)充一點(diǎn):父協(xié)程需要等待所有的子協(xié)程執(zhí)行完畢之后才會(huì)進(jìn)入Completed狀態(tài)锉矢,不管父協(xié)程自身的協(xié)程體是否已經(jīng)執(zhí)行完成。我們?cè)谧铋_始提到協(xié)程生命周期的時(shí)候就提到過下钞瀑,現(xiàn)在回過頭看是不是感覺很流程變得清晰沈撞。

                                      wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+
復(fù)制代碼

子協(xié)程會(huì)繼承父協(xié)程的協(xié)程上下文中的Element慷荔,如果自身有相同key的成員雕什,則覆蓋對(duì)應(yīng)的key,覆蓋的效果僅限自身范圍內(nèi)有效显晶。這個(gè)就可以用上我們前面學(xué)到的協(xié)程上下文CoroutineContext的知識(shí)贷岸,小案例奉上:

private fun  testCoroutineScope(){
    GlobalScope.launch(Dispatchers.Main){
        Log.d("父協(xié)程上下文", "$coroutineContext")
        launch(CoroutineName("第一個(gè)子協(xié)程")) {
            Log.d("第一個(gè)子協(xié)程上下文", "$coroutineContext")
        }
         launch(Dispatchers.Unconfined) {
            Log.d("第二個(gè)子協(xié)程協(xié)程上下文", "$coroutineContext")
        }
    }
}
復(fù)制代碼

日志順序的問題我們前面已經(jīng)分析過原因,如果還不懂的話磷雇,麻煩您回到基礎(chǔ)用法里面仔細(xì)的再看一遍偿警。

D/父協(xié)程上下文: [StandaloneCoroutine{Active}@81b6e46, Dispatchers.Main]
D/第二個(gè)子協(xié)程協(xié)程上下文: [StandaloneCoroutine{Active}@f6b7807, Dispatchers.Unconfined]
D/第一個(gè)子協(xié)程上下文: [CoroutineName(第一個(gè)子協(xié)程), StandaloneCoroutine{Active}@bbe6d34, Dispatchers.Main]
復(fù)制代碼

可以看到第一個(gè)子協(xié)程的覆蓋了父協(xié)程的Job,但是它繼承了父協(xié)程的調(diào)度器 Dispatchers.Main,同時(shí)也新增了一個(gè)CoroutineName唯笙。第二個(gè)子協(xié)程覆蓋了父協(xié)程的Job螟蒸,也將父協(xié)程的調(diào)度器覆蓋為Unconfined盒使,但是他沒有繼承第一個(gè)子協(xié)程的CoroutineName,這就是我們說的覆蓋的效果僅限自身范圍內(nèi)有效七嫌。接下來我們看看上面提到的協(xié)同作用域主從(監(jiān)督)作用域異常傳遞和協(xié)程取消的問題少办。

我們上面提到協(xié)同作用域如果子協(xié)程拋出未捕獲的異常時(shí),會(huì)將異常傳遞給父協(xié)程處理诵原,如果父協(xié)程被取消英妓,則所有子協(xié)程同時(shí)也會(huì)被取消。先上代碼看看效果:

private fun  testCoroutineScope2() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
    }
    GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
        Log.d("scope", "--------- 1")
        launch(CoroutineName("scope2") + exceptionHandler) {
            Log.d("scope", "--------- 2")
            throw  NullPointerException("空指針")
            Log.d("scope", "--------- 3")
        }
        val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
            Log.d("scope", "--------- 4")
            delay(2000)
            Log.d("scope", "--------- 5")
        }
        scope3.join()
        Log.d("scope", "--------- 6")
    }
}
復(fù)制代碼
D/scope: --------- 1
D/scope: --------- 2
D/exceptionHandler: CoroutineName(scope1) java.lang.NullPointerException: 空指針
復(fù)制代碼

可以看到子協(xié)程scope2拋出了一個(gè)異常绍赛,將異常傳遞給父協(xié)程scope1處理蔓纠,但是因?yàn)槿魏我粋€(gè)子協(xié)程異常退出會(huì)導(dǎo)致整體都將退出。所以導(dǎo)致父協(xié)程scope1未執(zhí)行完成成就被取消吗蚌,同時(shí)還未執(zhí)行完子協(xié)程scope3也被取消了腿倚。

主從(監(jiān)督)作用域協(xié)同作用域一致,區(qū)別在于該作用域下的協(xié)程取消操作的單向傳播性蚯妇,子協(xié)程的異常不會(huì)導(dǎo)致其它子協(xié)程取消猴誊。分析主從(監(jiān)督)作用域的時(shí)候,我們需要用到supervisorScope或者SupervisorJob侮措,如下代碼塊:

private fun testCoroutineScope3() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
    }
    GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
        supervisorScope {
            Log.d("scope", "--------- 1")
            launch(CoroutineName("scope2")) {
                Log.d("scope", "--------- 2")
                throw  NullPointerException("空指針")
                Log.d("scope", "--------- 3")
                val scope3 = launch(CoroutineName("scope3")) {
                    Log.d("scope", "--------- 4")
                    delay(2000)
                    Log.d("scope", "--------- 5")
                }
                scope3.join()
            }
            val scope4 = launch(CoroutineName("scope4")) {
                Log.d("scope", "--------- 6")
                delay(2000)
                Log.d("scope", "--------- 7")
            }
            scope4.join()
            Log.d("scope", "--------- 8")
        }
    }
}
復(fù)制代碼
D/scope: --------- 1
D/scope: --------- 2
D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指針
D/scope: --------- 6
D/scope: --------- 7
D/scope: --------- 8
復(fù)制代碼

可以看到子協(xié)程scope2拋出了一個(gè)異常懈叹,并將異常傳遞給父協(xié)程scope1處理,同時(shí)也結(jié)束了自己本身分扎。因?yàn)樵谟?code>主從(監(jiān)督)作用域下的協(xié)程取消操作是單向傳播性澄成,因此協(xié)程scope2的異常并沒有導(dǎo)致父協(xié)程退出,所以6 7 8都照常輸出畏吓,而3 4 5因?yàn)樵趨f(xié)程scope2里面所以沒有輸出墨状。

我們剛剛使用了supervisorScope實(shí)現(xiàn)了主從(監(jiān)督)作用域,那我們通過SupervisorJob又該如何實(shí)現(xiàn)呢。我們把supervisorScope稱之為主從(監(jiān)督)作用域菲饼,那么SupervisorJob就可以稱之為主從(監(jiān)督)作業(yè)肾砂,如下:

private fun testCoroutineScope4() {
    val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        Log.d("exceptionHandler", "${coroutineContext[CoroutineName]} $throwable")
    }
   val coroutineScope = CoroutineScope(SupervisorJob() +CoroutineName("coroutineScope"))
    GlobalScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {
        with(coroutineScope){
            val scope2 = launch(CoroutineName("scope2") + exceptionHandler) {
                Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")
                throw  NullPointerException("空指針")
            }
            val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {
                scope2.join()
                Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")
                delay(2000)
                Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")
            }
            scope2.join()
            Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")
            coroutineScope.cancel()
            scope3.join()
            Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")
        }
        Log.d("scope", "6--------- ${coroutineContext[CoroutineName]}")
    }
}
復(fù)制代碼
D/scope: 1--------- CoroutineName(scope2)
D/exceptionHandler: CoroutineName(scope2) java.lang.NullPointerException: 空指針
D/scope: 2--------- CoroutineName(scope3)
D/scope: 4--------- CoroutineName(coroutineScope)
D/scope: 5--------- CoroutineName(coroutineScope)
D/scope: 6--------- CoroutineName(scope1)
復(fù)制代碼

是不是感覺和supervisorScope的用法很像,我們通過創(chuàng)建了一個(gè)SupervisorJob的主從(監(jiān)督)協(xié)程作用域,調(diào)用了子協(xié)程的join是為了保證它一定是會(huì)執(zhí)行宏悦。同樣的子協(xié)程scope2拋出了一個(gè)異常镐确,通過協(xié)程scope2自己內(nèi)部消化了,同時(shí)也結(jié)束了自己本身饼煞。

因?yàn)閰f(xié)程scope2的異常并沒有導(dǎo)致coroutineScope作用域下的協(xié)程取消退出源葫,所以協(xié)程scope3照常運(yùn)行輸出2,后又因?yàn)檎{(diào)用了我們定義的協(xié)程作用域coroutineScopecancel方法取消了協(xié)程砖瞧,所以即使我們后面調(diào)用了協(xié)程scope3join,也沒有輸出3,因?yàn)?code>SupervisorJob的取消是向下傳播的息堂,所以后面的4 5都是在coroutineScope的作用域中輸出的。

現(xiàn)在我們關(guān)于協(xié)程作用域CoroutineScope的作用我們已經(jīng)有了一個(gè)大概的了解,同樣的因?yàn)檫@個(gè)篇幅中我們是基礎(chǔ)講解荣堰,所以我們點(diǎn)到為止床未,如果還想深入了解,那就只能看后面的深入?yún)f(xié)程篇幅振坚。

掛起函數(shù)

通過前面的篇幅我們已經(jīng)知道,使用suspend關(guān)鍵字修飾的函數(shù)叫作掛起函數(shù)即硼,掛起函數(shù)只能在協(xié)程體內(nèi),或著在其他掛起函數(shù)內(nèi)調(diào)用屡拨。那掛起又是啥玩意呢只酥。

我估計(jì)各位看到這里的時(shí)候,可能有些人已經(jīng)被上面的知識(shí)點(diǎn)弄的有點(diǎn)暈乎呀狼,別急裂允,先放松下大腦,喝杯水哥艇,然后做個(gè)眼保健操緩解一下绝编。下面開始敲黑板了,打起精神貌踏,要開始劃重點(diǎn)了十饥。

首先一個(gè)掛起函數(shù)既然要掛起,那么他必定得有一個(gè)掛起點(diǎn)祖乳,不然我們?cè)趺粗篮瘮?shù)是否掛起逗堵,從哪掛起呢。 我們定義一個(gè)空實(shí)現(xiàn)的suspend方法眷昆,然后通過AS的工具欄中Tools->kotlin->show kotlin ByteCode解析成字節(jié)碼

private suspend fun test(){
}
復(fù)制代碼
final synthetic test(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
復(fù)制代碼
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}
復(fù)制代碼

我們看到test方法需要的是一個(gè)Continuation接口蜒秤,官方給的介紹是用于掛起點(diǎn)之后,返回類型為T的值用的亚斋。那我們又是怎么拿到的這個(gè)Continuation呢作媚。要解開這個(gè)問題我們得先回到協(xié)程的創(chuàng)建和運(yùn)行是的過程。

我們啟動(dòng)一個(gè)協(xié)程無非是通過launch帅刊,async等方法纸泡。我們之前有說到過他們的啟動(dòng)模式CoroutineStart,但是并沒有深入的去分析它的創(chuàng)建和啟動(dòng)過程赖瞒,我們這里先回過頭大概的看一下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
復(fù)制代碼

我們看到在通過launch啟動(dòng)一個(gè)協(xié)程的時(shí)候女揭,他通過coroutinestart方法啟動(dòng)協(xié)程,然后我們接著往下看

public fun start(start: CoroutineStart, block: suspend () -> T) {
    initParentJob()
    start(block, this)
}

public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    initParentJob()
    start(block, receiver, this)
}
復(fù)制代碼

然后start方法里面調(diào)用了CoroutineStartinvoke冒黑,這個(gè)時(shí)候我們發(fā)現(xiàn)了Continuation田绑。

public operator fun <T> invoke(block: suspend () -> T, completion: Continuation<T>): Unit =
    when (this) {
        DEFAULT -> block.startCoroutineCancellable(completion)
        ATOMIC -> block.startCoroutine(completion)
        UNDISPATCHED -> block.startCoroutineUndispatched(completion)
        LAZY -> Unit // will start lazily
    }

public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>): Unit =
    when (this) {
        DEFAULT -> block.startCoroutineCancellable(receiver, completion)
        ATOMIC -> block.startCoroutine(receiver, completion)
        UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
        LAZY -> Unit // will start lazily
    }
復(fù)制代碼

Continuation又是通過start方法傳進(jìn)來的coroutine。所以現(xiàn)在可以確定抡爹,我們的協(xié)程體本身就是一個(gè)Continuation,這也就解釋了為什么可以在協(xié)程體內(nèi)調(diào)用suspend掛起函數(shù)了芒划。

現(xiàn)在我們也可以確定冬竟,在協(xié)程內(nèi)部掛起函數(shù)的調(diào)用處就是掛起點(diǎn)欧穴,如果掛起點(diǎn)出現(xiàn)異步調(diào)用,那么當(dāng)前協(xié)程就被掛起泵殴,直到對(duì)應(yīng)的Continuation通過調(diào)用resumeWith函數(shù)才會(huì)恢復(fù)協(xié)程的執(zhí)行涮帘,同時(shí)返回Result<T>類型的成功或者失敗的結(jié)果。

由于章節(jié)主題的限制笑诅,這里我們就不再下深入了调缨。需要注意的是掛起函數(shù)不一定真的會(huì)掛起,如果只是提供了掛起的條件吆你,但是協(xié)程沒有產(chǎn)生異步調(diào)用弦叶,那么協(xié)程還是不會(huì)被掛起。

預(yù)告:下一篇我們將會(huì)講解kotlin協(xié)程中的異常處理妇多,其實(shí)我們?cè)谶@篇章節(jié)中已經(jīng)伤哺,提到了一些異常處理,沒有注意的同學(xué)可以回到協(xié)程作用域看看者祖。

作者:一個(gè)被攝影耽誤的程序猿
來源:https://juejin.cn/post/6953287252373930021
著作權(quán)歸作者所有立莉。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處七问。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜓耻,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子械巡,更是在濱河造成了極大的恐慌媒熊,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坟比,死亡現(xiàn)場(chǎng)離奇詭異芦鳍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)葛账,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門柠衅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人籍琳,你說我怎么就攤上這事菲宴。” “怎么了趋急?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵喝峦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我呜达,道長(zhǎng)谣蠢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮眉踱,結(jié)果婚禮上挤忙,老公的妹妹穿的比我還像新娘。我一直安慰自己谈喳,他們只是感情好册烈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婿禽,像睡著了一般赏僧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扭倾,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天淀零,我揣著相機(jī)與錄音,去河邊找鬼吆录。 笑死窑滞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恢筝。 我是一名探鬼主播哀卫,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼撬槽!你這毒婦竟也來了此改?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤侄柔,失蹤者是張志新(化名)和其女友劉穎共啃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暂题,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡移剪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了薪者。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纵苛。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖言津,靈堂內(nèi)的尸體忽然破棺而出攻人,到底是詐尸還是另有隱情,我是刑警寧澤悬槽,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布怀吻,位于F島的核電站,受9級(jí)特大地震影響初婆,放射性物質(zhì)發(fā)生泄漏蓬坡。R本人自食惡果不足惜猿棉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望渣窜。 院中可真熱鬧铺根,春花似錦宪躯、人聲如沸乔宿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽详瑞。三九已至,卻和暖如春臣缀,著一層夾襖步出監(jiān)牢的瞬間坝橡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工精置, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留计寇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓脂倦,卻偏偏與公主長(zhǎng)得像番宁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子赖阻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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