【译文】扒一扒Kotlin Coroutines幕后实现

掘金迁移地址:【译文】扒一扒Kotlin Coroutines幕后实现

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

前言

如果你能看完本文并把所有内容都弄懂,你对协程的理解也已经超过大部分人了。

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)
}

其实我们一般执行回调,本质上都是callback,所以很多异步解决方案,追根溯源,会发现他的实现方式仍旧是callback。

不过callback的使用情境、context还有许许多多的用法情况都不同,整体概念也会有出入,所以我们会需要更多的名词来代表这样的情况,因此延伸出更多样的词汇,不过这段就题外话了。

话说回来,上面那段简易的callback,换作是Coroutine会变成是这样:

suspend fun longTimeProcess(parameter1:String):String{

val result =“”

//Do something long time

return result

}

这样写的好处是可以不用自己控制Thread的使用,上面的代码如果直接在主线程执行,可能会造成主线程卡顿,超过5秒喷Exception直接让Process out,所以还会需要额外自己开thread + handler或是使用Rxjava之类第三方套件去处理。换作是Coroutine,使用起来就简单很多了,被suspend修饰的函数longTimeProcess,有自己的作用域(Scope),用scope launch里头执行该function,利用这个function回传的数据做该在main thread上解决的事情,问题解决,就是如此的简单。

那问题来了。

Coroutine到底是怎么运作的?究竟是甚么神奇的魔法让他可以这么的方便可以不用写那么多东西呢?

记得某次面试里有提到这个问题,但我只知道他是个有限状态机,然后就…

恩,我那时的表情应该跟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的状态是否suspend,那因为上面这个程序很明显会是无限循环,多数我们在开发时会不需要无限的循环,那怎么样才能让这种来回传接球的形式有个终点呢?

答案就是有限状态机,接下来这篇文章会慢慢地解释。

有这么个东西叫做 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状态中,要被唤醒的callback

那注意这边提到状态了,大伙都知道Coroutine会是个状态机,那具体是怎么个状态呢?这个稍后提。

那如果硬要在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 "";
            }
        }
);

这样就行了吗?这样好像没啥效果最后会回一个空字串就是了,但这里就会发现,如果用lanuch会需要用到一个Function去传递一个continuation。这样看还是蒙,没关系,咱们继续看下去。

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);
}

字节码反编译成 Java 这种事,我们干过很多次了。跟往常不同的是,这次我不会直接贴反编译后的代码,因为如果我直接贴出反编译后的 Java 代码,估计会吓退一大波人。协程反编译后的代码,逻辑实在是太绕了,可读性实在太差了。没关系,我们直接梳理解释一下流程。

反编译代码中我们看到一个 switch(this.label) , 这就是大名鼎鼎的 Coroutine状态机了,Kotlin编译器会在编译时产生一个label,这个label就是runBlock里边执行到第几段的状态了。

那具体会有几个状态呢?其实在runBlock里边有几个suspend就会对应有几个状态机,举个例子:

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 的状态,所以需要五个执行状态的,case 0 用于初始化,case 1– 4 用于结果获取。

那状态何时会改变呢?

答案是:invokeSuspend 执行时。

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;
}

这部分比较有意思的地方是,这些状态还有 call method 的都不在 switch case 里面,这其实跟 Bytecode 有关,主要是因为这个结果是 反编译 出來的东西,所以会是这样的叠加方式。

我们可以看到,在状态机改变时:

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

根据上述代码可以看出, 编译器内部有一个函数IntrinsicsKt.getCOROUTINE_SUSPENDED() 该函数代表当前的状态是否挂起。如果它回传的是 getCOROUTINE_SUSPENDED,代表这个 function 处在 挂起(suspended)的状态,意味着它可能当前正在进行耗时操作。这时候直接返回 挂起状态,等待下一次被 调用(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 被创建时就会执行(所以需要 case 0 做初始化),可以看到 invokeSuspend会被执行到。(probeCoroutineResumed 那個看起來是 debug 用的請無視),通过执行 invokeSuspend 开始执行状态机,如果该 continuation 的状态是挂起,就会执行return,重新执行 invokeSuspend,等下一次被唤醒,再次被唤醒后,继续执行,直到得到结果,并且将结果通过 continuation((name in completion)resumeWith返回结果,结束此次执行,接着继续执行挂起函数的的 invokeSuspend ,如此反复直至最终结束。

到這裡,我們知道了, suspend标记的函数内部是通过状态机才实现的挂起恢复的,并且利用状态机来记录Coroutine执行的状态

执行挂起函数时可以得到它的状态为:getCOROUTINE_SUSPENDED

不过又有问题来了,当挂起函数判断条件为:getCOROUTINE_SUSPENDED时执行了 return,代表它已经结束了,那它怎么能继续执行呢?而且还有办法在执行完后通知协程。

这里我们拿一段代码来看看:

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);
}

会发现,该函数 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 的部分生成一個有限的状态机,并包装进 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 执行的时候执行。

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) 的时候,会执行到 suspendFunction(String),并传入包裝好的 Continuation

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

suspendFunction 自己本身也是一個挂起函数,所以它也会包裝成一个 Continuation(但这边就单纯很多,虽然也会生成状态机,但其实就是直接跑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,让原先执行该挂起函数的Threadreturn 并执行其他东西,而 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 把结果传入,这个动作会执行到挂起函数的invokeSuspend(result),并传入结果,该动作就能让挂起函数得到suspendFunction(String)的结果。

PS:上面那段代码实际上是伪代码,实际业务会比这复杂的多

所以事实上,挂起函数就是我把我的 callback 給你,等你结束后再用我之前给你的 callback 回调给我,你把你的 callback 給我,等我结束后我用之前你给我的 callback 通知你。

挂起函数时如何自行切换线程的?

原则上,挂起函数在执行时,就会决定好要用哪个 Dispatcher,然后就会建立挂起点,一般情况下,会走到 startCoroutineCancellable,然后执行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 其实就是我们前面说到的初始化操作, 這行会去执行状态机 case 0

至于 intercepted() ,到底要拦截啥,其实就是把生成的 Continuation 拦截给指定的 ContinuationInterceptor (这东西包裝在 CoroutineContext 里面,原则上在指定 Dispatcher 的时候就已经建立好了)

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

这里可以注意到 interceptContinuation(Continuation) ,可以用他追下去,发现他是 ContinuationInterceptor 的方法 ,再追下去可以发现CoroutineDispatcher 继承了他:

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

可以发现该动作产生了一个 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,沒有就直接执行了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

那回头看一下,其实就可以发现是 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 的执行:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容