【譯文】扒一扒Kotlin Coroutines幕后實現

掘金遷移地址:【譯文】扒一扒Kotlin Coroutines幕后實現

原文地址: Kotlin Coroutines 幕後那一兩件事

前言

如果你能看完本文并把所有內容都弄懂,你對協(xié)程的理解也已經超過大部分人了。

Coroutines是近幾年在Kotlin上Google主推的異步問題解決方案,至少在Android R Asynctask被放棄后,打開Android Document看到最顯目的提示項目就是導引你至Coroutine的頁面教導你怎么使用Coroutine。

Emm….那如果把所有問題簡單化,其實大多數碰上異步問題,解決的辦法基本上都是callback。

fun longTimeProcess(parameter1 : String, callBack:CallBack<String>){
    val result = ""
    //Do something long time
    callBack.onResponse(result)
}

其實我們一般執(zhí)行回調,本質上都是callback,所以很多異步解決方案,追根溯源,會發(fā)現他的實現方式仍舊是callback。

不過callback的使用情境、context還有許許多多的用法情況都不同,整體概念也會有出入,所以我們會需要更多的名詞來代表這樣的情況,因此延伸出更多樣的詞匯,不過這段就題外話了。

話說回來,上面那段簡易的callback,換作是Coroutine會變成是這樣:

suspend fun longTimeProcess(parameter1:String):String{

val result =“”

//Do something long time

return result

}

這樣寫的好處是可以不用自己控制Thread的使用,上面的代碼如果直接在主線程執(zhí)行,可能會造成主線程卡頓,超過5秒噴Exception直接讓Process out,所以還會需要額外自己開thread + handler或是使用Rxjava之類第三方套件去處理。換作是Coroutine,使用起來就簡單很多了,被suspend修飾的函數longTimeProcess,有自己的作用域(Scope),用scope launch里頭執(zhí)行該function,利用這個function回傳的數據做該在main thread上解決的事情,問題解決,就是如此的簡單。

那問題來了。

Coroutine到底是怎么運作的?究竟是甚么神奇的魔法讓他可以這么的方便可以不用寫那么多東西呢?

記得某次面試里有提到這個問題,但我只知道他是個有限狀態(tài)機,然后就…

恩,我那時的表情應該跟King Crimson有那么幾分神似就是了。

Coroutine概念

維基百科上其實有解釋了Coroutine的實作概念:

var q := new queue
coroutine produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield to consume



coroutine consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield to produce

概念是,這有個queue是空的,那是先跑coroutine product還是coroutine consume其實無所謂,總之隨便跑一個,先從coroutine product開始好了。

coroutine produce在queue沒滿時,會產生一些items,然后加入queue里頭,直到queue滿為止,接著把程序讓給coroutine consume。

coroutine consume在queue不是空的時候,會移除(消費)一些items,直到queue空為止,接著把程序讓給coroutine produce,如此反復,這個世界的經濟得以維持。

那這邊可以看出,當coroutine produce碰到queue是滿的時候會直接把程序讓給coroutine consume;相對的,若coroutine consume在碰到queue是空的時候,會直接把程序讓給coroutine produce

那么,以Kotlin Coroutine來說,queue的是空是滿的條件會變成是method的狀態(tài)是否suspend,那因為上面這個程序很明顯會是無限循環(huán),多數我們在開發(fā)時會不需要無限的循環(huán),那怎么樣才能讓這種來回傳接球的形式有個終點呢?

答案就是有限狀態(tài)機,接下來這篇文章會慢慢地解釋。

有這么個東西叫做 Continuation

很多時候,原本很麻煩的事情突然變得簡單了,其實不是什么都不用做,而是事情有人幫你做了,Coroutine也是,它幫你把寫一堆callback的麻煩事給做掉了。

等等,Compiler把寫一堆的callback的麻煩事給做掉了,那意思是…

沒錯,Coroutine本質上還是callback,只是編譯器幫你寫了。

我本來是想說從CoroutineScope.Launch下去追的,追到IntrinsicsJvm,這東西叫Intrinsic這東西有很大的機率是給編譯器用的,追到這里,大概就可以知道,suspend fun會在編譯的過程轉成Continuation.

但后來換個方向去想,其實也不用這么麻煩,因為Kotlin是可以給Java呼叫的,那Java比較少這種語法糖轉譯的東西,也就是說,透過Java呼叫suspend fun,就可以知道suspend fun真正的模樣。

這邊先隨便寫一個suspend fun。

suspend fun getUserDescription(name:String,id:Int):String{
    return ""
}

在 Java 中調用的時候是如下這樣:

instance.getUserDescription("name", 0, new Continuation<String>() {
    @NotNull
    @Override
    public CoroutineContext getContext() {
        return null;
    }

    @Override
    public void resumeWith(@NotNull Object o) {

    }
});
return 0;

我們可以看到,其實suspend fun就是一般的function后頭加上一個Continuation

總之得到一個線索,這個線索就是Continuation,它是個什么玩意呢?

它是一個 interface

public interface Continuation<in T> {
    public val context: CoroutineContext

    public fun resumeWith(result: Result<T>)
}

它代表的是CoroutinerunBlocksuspend狀態(tài)中,要被喚醒的callback

那注意這邊提到狀態(tài)了,大伙都知道Coroutine會是個狀態(tài)機,那具體是怎么個狀態(tài)呢?這個稍后提。

那如果硬要在java file里頭使用GlobalScope.launch,那會長成這樣:

BuildersKt.launch(GlobalScope.INSTANCE,
        Dispatchers.getMain(),//context to be ran on
        CoroutineStart.DEFAULT,
        new Function2<CoroutineScope,Continuation<? super Unit>,String>() {
            @Override
            public String invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {

                return "";
            }
        }
);

這樣就行了嗎?這樣好像沒啥效果最后會回一個空字串就是了,但這里就會發(fā)現,如果用lanuch會需要用到一個Function去傳遞一個continuation。這樣看還是蒙,沒關系,咱們繼續(xù)看下去。

Continuation到底怎么運行?

那這邊簡單用一個suspend:

fun main() {
    GlobalScope.launch {
        val text = suspendFunction("text")
        println(text) // print after delay
    }
}
suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){

    val result = doSomethingLongTimeProcess(text)
    result
}

Kotlin Bytecodedecompile 會得到這個:

public static final void main() {
   BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
      private CoroutineScope p$;
      Object L$0;
      int label;

      @Nullable
      public final Object invokeSuspend(@NotNull Object $result) {
         Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
         Object var10000;
         CoroutineScope $this$launch;
         switch(this.label) {
         case 0:
            ResultKt.throwOnFailure($result);
            $this$launch = this.p$;
            this.L$0 = $this$launch;
            this.label = 1;
            var10000 = CoroutineTestKt.suspendFunction("text", this);
            if (var10000 == var5) {
               return var5;
            }
            break;
         case 1:
            $this$launch = (CoroutineScope)this.L$0;
            ResultKt.throwOnFailure($result);
            var10000 = $result;
            break;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
         }

         String text = (String)var10000;
         boolean var4 = false;
         System.out.println(text);
         return Unit.INSTANCE;
      }

      @NotNull
      public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
         Intrinsics.checkParameterIsNotNull(completion, "completion");
         Function2 var3 = new <anonymous constructor>(completion);
         var3.p$ = (CoroutineScope)value;
         return var3;
      }

      public final Object invoke(Object var1, Object var2) {
         return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
      }
   }), 3, (Object)null);
} 

另外一個是 suspendFunctiondecompile code

public static final Object suspendFunction(@NotNull final String text, @NotNull Continuation $completion) {
   return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
      private CoroutineScope p$;
      int label;

      @Nullable
      public final Object invokeSuspend(@NotNull Object $result) {
         Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
         switch(this.label) {
         case 0:
            ResultKt.throwOnFailure($result);
            CoroutineScope $this$withContext = this.p$;
            String result = CoroutineTestKt.doSomethingLongTimeProcess(text);
            return result;
         default:
            throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
         }
      }

      @NotNull
      public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
         Intrinsics.checkParameterIsNotNull(completion, "completion");
         Function2 var3 = new <anonymous constructor>(completion);
         var3.p$ = (CoroutineScope)value;
         return var3;
      }

      public final Object invoke(Object var1, Object var2) {
         return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
      }
   }), $completion);
}

字節(jié)碼反編譯成 Java 這種事,我們干過很多次了。跟往常不同的是,這次我不會直接貼反編譯后的代碼,因為如果我直接貼出反編譯后的 Java 代碼,估計會嚇退一大波人。協(xié)程反編譯后的代碼,邏輯實在是太繞了,可讀性實在太差了。沒關系,我們直接梳理解釋一下流程。

反編譯代碼中我們看到一個 switch(this.label) , 這就是大名鼎鼎的 Coroutine狀態(tài)機了,Kotlin編譯器會在編譯時產生一個label,這個label就是runBlock里邊執(zhí)行到第幾段的狀態(tài)了。

那具體會有幾個狀態(tài)呢?其實在runBlock里邊有幾個suspend就會對應有幾個狀態(tài)機,舉個例子:

GlobalScope.launch {
        test()
        test()
        test()
        test()
}
fun test(){}

如上代碼會有幾個呢?

答案是一個,因為這 test() 不是掛起函數(suspend function),它不需要掛起操作(suspended)。

如果換成是這樣?

GlobalScope.launch {
        test()
        test()
        test()
        test()
}
suspend fun test(){}

答案是五個。

GlobalScope.launch {
        // case 0
        test() // case 1 receive result
        test() // case 2 receive result
        test() // case 3 receive result
        test() // case 4 receive result
}

因為四個 test() 都有可能獲得 suspended 的狀態(tài),所以需要五個執(zhí)行狀態(tài)的,case 0 用于初始化,case 1– 4 用于結果獲取。

那狀態(tài)何時會改變呢?

答案是:invokeSuspend 執(zhí)行時。

label34: {
   label33: {
      var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(this.label) {
      case 0:
         ResultKt.throwOnFailure($result);
         $this$launch = this.p$;
         this.L$0 = $this$launch;
         this.label = 1;
         if (CoroutineTestKt.test(this) == var3) {
            return var3;
         }
         break;
      case 1://...ignore
         break;
      case 2://...ignore
         break label33;
      case 3://...ignore
         break label34;
      case 4://...ignore
         return Unit.INSTANCE;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }
      this.L$0 = $this$launch;
      this.label = 2;
      if (CoroutineTestKt.test(this) == var3) {
         return var3;
      }
   }

   this.L$0 = $this$launch;
   this.label = 3;
   if (CoroutineTestKt.test(this) == var3) {
      return var3;
   }
}

this.L$0 = $this$launch;
this.label = 4;
if (CoroutineTestKt.test(this) == var3) {
   return var3;
} else {
   return Unit.INSTANCE;
}

這部分比較有意思的地方是,這些狀態(tài)還有 call method 的都不在 switch case 里面,這其實跟 Bytecode 有關,主要是因為這個結果是 反編譯 出來的東西,所以會是這樣的疊加方式。

我們可以看到,在狀態(tài)機改變時:

Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
//...ignore
    
    this.label = 1;
    if (CoroutineTestKt.test(this) == var3) {
       return var3;
    }

根據上述代碼可以看出, 編譯器內部有一個函數IntrinsicsKt.getCOROUTINE_SUSPENDED() 該函數代表當前的狀態(tài)是否掛起。如果它回傳的是 getCOROUTINE_SUSPENDED,代表這個 function 處在 掛起(suspended)的狀態(tài),意味著它可能當前正在進行耗時操作。這時候直接返回 掛起狀態(tài),等待下一次被 調用(invoke)

那什么時候會再一次被 調用(invoke) 呢?

這時候就要看傳入到該掛起函數的的 Continuation ,這裡可以觀察一下 BaseContinuationImplresumeWith 的操作:

internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
 public final override fun resumeWith(result: Result<Any?>) {
    var current = this
    var param = result
    while (true) {
        probeCoroutineResumed(current)
        with(current) {
            val completion = completion!!
            val outcome: Result<Any?> =
                try {
                    val outcome = invokeSuspend(param)
                    if (outcome === COROUTINE_SUSPENDED) return
                    Result.success(outcome)
                } catch (exception: Throwable) {
                    Result.failure(exception)
                }
            releaseIntercepted()
            if (completion is BaseContinuationImpl) {
                current = completion
                param = outcome
            } else {
                completion.resumeWith(outcome)
                return
            }
        }
    }
 }
//...ignore
}

原則上 resumeWith 在一開始 Coroutine 被創(chuàng)建時就會執(zhí)行(所以需要 case 0 做初始化),可以看到 invokeSuspend會被執(zhí)行到。(probeCoroutineResumed 那個看起來是 debug 用的請無視),通過執(zhí)行 invokeSuspend 開始執(zhí)行狀態(tài)機,如果該 continuation 的狀態(tài)是掛起,就會執(zhí)行return,重新執(zhí)行 invokeSuspend,等下一次被喚醒,再次被喚醒后,繼續(xù)執(zhí)行,直到得到結果,并且將結果通過 continuation((name in completion)resumeWith返回結果,結束此次執(zhí)行,接著繼續(xù)執(zhí)行掛起函數的的 invokeSuspend ,如此反復直至最終結束。

到這裡,我們知道了, suspend標記的函數內部是通過狀態(tài)機才實現的掛起恢復的,并且利用狀態(tài)機來記錄Coroutine執(zhí)行的狀態(tài)

執(zhí)行掛起函數時可以得到它的狀態(tài)為:getCOROUTINE_SUSPENDED

不過又有問題來了,當掛起函數判斷條件為:getCOROUTINE_SUSPENDED時執(zhí)行了 return,代表它已經結束了,那它怎么能繼續(xù)執(zhí)行呢?而且還有辦法在執(zhí)行完后通知協(xié)程。

這里我們拿一段代碼來看看:

suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){

    val result = doSomethingLongTimeProcess(text) 
    result //result 是個 String
}

它 decompile 後:

public static final Object suspendFunction(@NotNull final String text, @NotNull Continuation $completion) {
   return BuildersKt.withContext(
(CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
//...ignore
   }), $completion);
}

會發(fā)現,該函數 return 的不是 String而是一個Object,那這個Object是什么呢?其實就是COROUTINE_SUSPENDED

要證明這點其實很簡單,如下代碼,調用該 suspendFunction 就可以了

Object text = instance.suspendFunction("", new Continuation<String>() {
    @NotNull
    @Override
    public CoroutineContext getContext() {
        return Dispatchers.getMain();
    }

    @Override
    public void resumeWith(@NotNull Object o) {

    }
});
System.out.println(text);

結果:

COROUTINE_SUSPENDED

Process finished with exit code 0

PS:如果該函數時一個普通函數,沒有標記suspend 則會直接返回結果。

根據上邊我們這么多的分析,我們可以解釋那段代碼了。

fun main() {
    GlobalScope.launch {
        val text = suspendFunction("text")
        println(text) // print after delay
    }

}

suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){

    val result = doSomethingLongTimeProcess(text)
    result
}

首先,Kotlin編譯器會把 main() 里面的代碼反編譯生成一個Continuation,而 launch block 的部分生成一個有限的狀態(tài)機,并包裝進 Continuation 里面那個叫 invokeSuspend(result) 的方法里頭,并做為初次 resumeWith

Continuation { // GlobalScope.Lanuch()
    var label = 0
    fun invokeSuspend(result:Any):Any{
        when(label){
            0->{
                val functionResult = suspendFunction("text",this)
                lable = 1
                if(functionResult == COROUTINE_SUSPENDED){
                    return functionResult
                }
            }
            1->{
                throwOnFailure(result)
                break
            }
        }
        val text = result as String
        print(text)
    }
}

invokeSuspend(result) 會在該 ContinuationresumeWith 執(zhí)行的時候執(zhí)行。

Continuation { // GlobalScope.Lanuch()
    var label = 0
    fun invokeSuspend(result:Any):Any{
        when(label){
            0->{
                val functionResult = suspendFunction("text",this)
                lable = 1
                if(functionResult == COROUTINE_SUSPENDED){
                    return functionResult
                }
            }
            1->{
                throwOnFailure(result)
                break
            }
        }
        val text = result as String
        print(text)
    }
}

第一次執(zhí)行 invokeSuspend(result) 的時候,會執(zhí)行到 suspendFunction(String),并傳入包裝好的 Continuation

Continuation { // suspendFunction(text)
    fun invokeSuspend(result:Any):Any{
        when(label){
            0->{
                val text = doSomethingLongTimeProcess(context)
                return 後執(zhí)行 continuation.resultWith(text)
                
            }
        }
    }
}

suspendFunction 自己本身也是一個掛起函數,所以它也會包裝成一個 Continuation(但這邊就單純很多,雖然也會生成狀態(tài)機,但其實就是直接跑doSomethingLongTimeProcess())。

Continuation { // GlobalScope.Lanuch()
    var label = 0
    fun invokeSuspend(result:Any):Any{
        when(label){
            0->{
                val functionResult = suspendFunction("text",this)
                lable = 1
                if(functionResult == COROUTINE_SUSPENDED){
                    return functionResult
                }
            }
            1->{
                throwOnFailure(result)
                break
            }
        }
        val text = result as String
        print(text)
    }
}

因為會進行耗時操作,所以直接回傳COROUTINE_SUSPENDED,讓原先執(zhí)行該掛起函數的Threadreturn 并執(zhí)行其他東西,而 suspendFunction則在另一條 Thread上把耗時任務完成。

Continuation { // GlobalScope.Lanuch()
    var label = 0
    fun invokeSuspend(result:Any):Any{
        when(label){
            0->{
                val functionResult = suspendFunction("text",this)
                lable = 1
                if(functionResult == COROUTINE_SUSPENDED){
                    return functionResult
                }
            }
            1->{
                throwOnFailure(result)
                break
            }
        }
        val text = result as String
        print(text)
    }
}

等待 suspendFunction 的耗時任務完成后,利用傳入的 ContinuationresumeWith 把結果傳入,這個動作會執(zhí)行到掛起函數的invokeSuspend(result),并傳入結果,該動作就能讓掛起函數得到suspendFunction(String)的結果。

PS:上面那段代碼實際上是偽代碼,實際業(yè)務會比這復雜的多

所以事實上,掛起函數就是我把我的 callback 給你,等你結束后再用我之前給你的 callback 回調給我,你把你的 callback 給我,等我結束后我用之前你給我的 callback 通知你。

掛起函數時如何自行切換線程的?

原則上,掛起函數在執(zhí)行時,就會決定好要用哪個 Dispatcher,然后就會建立掛起點,一般情況下,會走到 startCoroutineCancellable,然后執(zhí)行createCoroutineUnintercepted,也就是上面提到的:resumeWithinvokeSuspend

我們進入到startCoroutineCancellable內部再看看:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit))
    }

createCoroutineUnintercepted 最后會產出一個 Continuation ,而resumeCancellableWith 其實就是我們前面說到的初始化操作, 這行會去執(zhí)行狀態(tài)機 case 0

至于 intercepted() ,到底要攔截啥,其實就是把生成的 Continuation 攔截給指定的 ContinuationInterceptor (這東西包裝在 CoroutineContext 里面,原則上在指定 Dispatcher 的時候就已經建立好了)

public fun intercepted(): Continuation<Any?> =
    intercepted
        ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
            .also { intercepted = it }

這里可以注意到 interceptContinuation(Continuation) ,可以用他追下去,發(fā)現他是 ContinuationInterceptor 的方法 ,再追下去可以發(fā)現CoroutineDispatcher 繼承了他:

public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
    DispatchedContinuation(this, continuation)

可以發(fā)現該動作產生了一個 DispatchedContinuation,看看 DispatchedContinuation ,可以注意到剛才有提到的 resumeCancellableWith

inline fun resumeCancellableWith(result: Result<T>) {
    val state = result.toState()
    if (dispatcher.isDispatchNeeded(context)) {
        _state = state
        resumeMode = MODE_CANCELLABLE
        dispatcher.dispatch(context, this)
    } else {
        executeUnconfined(state, MODE_CANCELLABLE) {
            if (!resumeCancelled()) {
                resumeUndispatchedWith(result)
            }
        }
    }
}

原則上就是利用 dispatcher 來決定需不需要 dispatch,沒有就直接執(zhí)行了resumeUndispatchedWith

@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
inline fun resumeUndispatchedWith(result: Result<T>) {
    withCoroutineContext(context, countOrElement) {
        continuation.resumeWith(result)
    }
}

其實就是直接跑 continuationresumeWith

那回頭看一下,其實就可以發(fā)現是 CoroutineDispatcher 決定要用什么 Thread 了。

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    @InternalCoroutinesApi
    public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block)
    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
        DispatchedContinuation(this, continuation)
    @InternalCoroutinesApi
    public override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
        (continuation as DispatchedContinuation<*>).reusableCancellableContinuation?.detachChild()
    }
}

其實知道這個東西后,就可以向下去找它的 Child ,就能找到 HandlerDispatcher 了。

isDispatchNeeded 就是說是否需要切換線程

dispatch 則是切換線程的操作

可以看到這兩個方法在 HandlerDispatcher 的執(zhí)行:

override fun isDispatchNeeded(context: CoroutineContext): Boolean {
    return !invokeImmediately || Looper.myLooper() != handler.looper
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
    handler.post(block)
}

可以看到CoroutineContext根本沒有用到。

為什么呢?其實原因主要是: 掛起函數是設計給 Kotlin 用的,并不是專門設計給 Android用的,所以 Android 要用的話,還是需要實現 CoroutineDispatcher 的部分,這實際上是兩個體系的東西。那 CoroutineDispatcherdispatch 有提供 CoroutineContext,但不見的 Android 這邊會用到,所以就有這個情況了。

其他諸如 Dispatcher.Default ,他用到了 線程池(Executor)Dispatcher.IO 則是用到了一個叫 工作隊列(WorkQueue) 的東西。

所以每一個 Dispatcher 都有自己的一套實現,目前有提供四種 Dispatcher

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

推薦閱讀更多精彩內容