Android消息机制Handler源码分析

概述

作为Android开发程序员,在开发应用的过程中我们最常用的也是第一个接触到的消息机制就是Handler。因为Android应用程序是通过消息机制来驱动的,Android某种意义上也可以说成是一个以消息驱动的系统,UI、事件、生命周期都和消息处理机制息息相关,并且消息处理机制在整个Android知识体系中也是尤其重要。本篇文章仅仅是针对自己之前学习和使用到Handler的一个归纳总结

将带着下列10个问题出发,通过Handler源码来寻找答案

1.Handler源码中的Looper、Handler、MessageQueue,Message之间的关系
2.一个线程有几个Handler?
3.一个线程有几个Looper?如何保证?
4.Handler内存泄漏原因?为什么其他的内部类没有说过这个问题?
5.为何主线程可以new Handler?如果想要在子线程中new Handler 要做些什么准备?
6.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么作用?
7.既然可以存在多个Handler往MessageQueue中添加数据(发消息是各个Handler可能处于不同线程),那么内部如何保证线程安全?
8.我们使用Message是应该如何创建它?
9.使用Handler的postDealy后消息队列会有什么变化?
10.Looper死循环为什么不会导致应用卡死?

1.Handler源码中的Looper、Handler、MessageQueue,Message之间的关系?

​ 我们时常有这样的一个疑问?Handler内部的消息转发是怎样做的,内部是什么样的一个搬运过程?在生活中我们经常坐地铁,我们从地面到地下等车区,经常使用的电梯,也像健身房里的跑步机。Handler的内部运行原理和这个是类似的。用一个图可能更加直观明了。这就是Android的一种设计模式---Handler之生产者/消费者设计模式。


Looper、Handler、MessageQueue、Message之间关系

通过查看Handler源码我们发现,在Handler里面有14个方法,
Handler主要函数

从图中我们就能看出不管我们怎么调用Handler里面的方法,最终都是走到enqueueMessage里面来

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

这个方法里面我们发现最主要是for循环,这个for循环是什么呢?它是一个单列表轮询,

在轮询过程中发现下一个Message是null或者发现这个时间节点比我们新加入的时间节点晚或者这个时候的时间节点为0,这个时候我们将这个新加入的时间节点Message加入到单列表中。



通过这个我们就知道我们是总将货物放到传送带上去的,这就是我们通过sendmessage将message插入MessageQueue队列里面。(send --》messageQueue.enqueueMessage --》messageQueue
添加消息)
我们又是怎样得到Message的消息的呢,通过MessageQueue代码里面的next方法发现它返回的就是Message,那它是怎样去获取的呢?


通过当前的时间点和队列的队头的时间点进行比较,如果当前时间小于队头的时间点,就需要等待,等待当前时间点大于或者等于队头的时间点,就return 这个时间点对应的Message。那是谁不停的调用next函数呢?看过源码的都知道,原来是Looper里面的loop函数,通过Looper的代码发现loop这个方法,这个loop就是一个for循环,而且这个for循环是一个死循环,有人会说那它一定会调用break方法,其实不然,通过代码我们发现它原来是通过next方法获取Message,如果这个message为空null的时候,会等待,会死锁

没有消息就会一直等待。

/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        boolean slowDeliveryDetected = false;

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

消息进来:(send --》messageQueue.enqueueMessage --》messageQueue
添加消息)
消息出去:(Looper--》loop--》messageQueue--》next(取出消息)---》dispatchMessage--》handlerMessage)
总结:我们在发送消息的时候是将消息插入到队列里面来,然后通过Looper这个发动机带动传送带转动,从而将消息滚动起来,next将当前的时间点大于等于出队列的消息时间点的时候返回出来,loop就会将这个消息传输出来。
这个时候Looper、Handler、MessageQueue、Message都出来了。
这个时候我们就可以解答下面的问题了。

2.一个线程有几个Handler?

A:n个,想要的时候可以通过new进行创建,想要多少就要多少

3.一个线程有几个Looper?如何保证?

1.从Looper.prepare()开始
要在一个线程里面处理消息,代码如下:

class LooperThread extends Thread
{
public Handler mHandler;
public void run() 
{
Looper.prepare();
mHandler = new Handler() 
{
public void handleMessage(Message msg) 
{
// process incoming messages here
}
};
Looper.loop();
}

首先就必须要先调用Looper.prepare(),那这个方法做了些什么呢:

代码其实只有关键性的一句,就是sThreadLocal.set(new Looper(quitAllowed)),首先来看看sThreadLocal

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
ThreadLocal:代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。

在这里ThreadLocal的作用是保证了每个线程都有各自的Looper,ThreadLocal里面是如何保存的呢,原来它是通过键值对的形式保存的,那如何保证每个线程只有一个Looper的呢,通过源码我们发现只有一个地方调用了sThreadLocal.set方法,所以想要修改这个函数的值,只能进行set

private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

如果已经创建了则不能创建(Only one Looper may be created per thread)
那ThreadLocal是怎么set的呢

public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }
   这个values(currentThread)又是什么呢,这个values是什么函数,它获取的是线程的localValues,也就是说一个线程只有一个这样的values,而这个values我们只会用到一次,这样就保证线程和Looper的唯一性,所以一个线程只能有一个Looper。
    上面的判断也说明了一个问题:一个线程只能有一个Looper。

4.Handler内存泄漏原因?为什么其他的内部类没有说过这个问题?

内存泄露的定义:本该被回收的对象不能被回收而停留在堆内存中
内存泄露出现的原因:当一个对象已经不再被使用时,本该被回收但却因为有另外一个正在使用的对象持有它的引用从而导致它不能被回收。
这就导致了内存泄漏。

  Handler的一般用法 = 新建Handler子类(内部类) 、匿名Handler内部类

为啥其他的内部类不会,而Handler会呢?这个是Handler原来导致的

<meta charset="utf-8">

  • Handler实例的消息队列有2个分别来自线程1、2的消息(分别 为延迟1s6s
  • Handler消息队列 还有未处理的消息 / 正在处理消息时,消息队列中的Message持有Handler实例的引用
  • 由于Handler = 非静态内部类 / 匿名内部类(2种使用方式),故又默认持有外部类的引用(即MainActivity实例),引用关系如下图

    通过这个我们发现原来在enqueueMessage的时候,就将this保存起来了。引用关系会一直保持,直到Handler消息队列中的所有消息被处理完毕
  • Handler消息队列 还有未处理的消息 / 正在处理消息时,此时若需销毁外部类MainActivity,但由于上述引用关系,垃圾回收器(GC)无法回收MainActivity,从而造成内存泄漏。如下图:

5.为何主线程可以new Handler?如果想要在子线程中new Handler 要做些什么准备?


主线程为啥可以直接new Handler,原因是在ActivityThread的main函数中会直接 Looper.prepareMainLooper()以及 Looper.loop(),prepare是干嘛的呢?


用来初始化一个Looper,接下来调用了一个Looper的loop,进而执行了MessageQueue的next方法,这样就将传送带滚动起来。

主线程的所有代码全面执行在
这个里面,全部是通过Handler消息进行传递。


通过代码发现原来activity的所有消息都是在这里进行处理的

如果想要在子线程中new Handler 要做些什么准备?通过上面的问题分析发现,必须需要先prepare()和loop()

6.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么作用?

​ 我们先来创建一个子线程,然后子线程中创建了一个Looper,并且发送了一个消息,消息处理完了,看一下会发生什么样的事情。

 Handler threadHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (threadHandler == null){
                    Looper.prepare();
                    threadHandler = new Handler(){
                        @Override
                        public void handleMessage(Message msg) {
                            super.handleMessage(msg);
                            Log.e("111","handle Message");
                        }
                    };
                    Log.e("111","Looper loop() 1");
                    Looper.loop();
                    Log.e("111","Looper loop() 2");
                }
            }
        }).start();
 
    }
 
    public void click(View view){
        Log.e("seas","click");
        threadHandler.sendMessage(threadHandler.obtainMessage());
 
    }

我运行之后,再点击执行click方法,就出现了如下的log。

如果这个子线程结束,那么他应该是有调用到?Log.e("111","Looper loop() 2");这个语句才对,但是它并没有,这就意味着这个Looper一直处于一个阻塞状态。

也就是说它一直卡在了这个地方:

        for (;;) {            
              Message msg = queue.next(); // might block             
       .....        
     }

所以它就一直在等待,这个子线程也已经干不了其他的事情了,其实也被卡死了。
子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?

在Handler机制里面有一个Looper,在Looper机制里面有一个函数,叫做quitSafely()和quit()函数,这两个函数是调用的MessageQueue的quit()。
再进入到MessageQueue的quit()函数。



它会remove消息,把消息队列中的全部消息给干掉。

把消息全部干掉,也就释放了内存。



而在quit()函数的最后一行,有一个nativeWake()函数。



这个函数的调用,就会叫醒等待的地方,醒来之后,就接着往下执行。
       //native的方法,在没有消息的时候回阻塞管道读取端,只有nativePollOnce返回之后才能往下执行
          //阻塞操作,等待nextPollTimeoutMillis时长
          nativePollOnce(ptr, nextPollTimeoutMillis);

next函数会往下执行,然后会发现 Message msg = mMessages; 是空的,然后就执行了这个,就接着往下走。

                if (msg != null) {
 
                   ......
 
                } else {
                    // No more messages.
                    //没有消息,nextPollTimeoutMillis复位
                    nextPollTimeoutMillis = -1;
                }

然后又调用了这个方法,并且return了null。

                // Process the quit message now that all pending messages have been handled.
                //如果消息队列正在处于退出状态返回null,调用dispose();释放该消息队列
                if (mQuitting) {
                    dispose();
                    return null;
                }

所以说,这个时候Looper就结束了(跳出了死循环),则达成了第二个作用:释放线程
在上面我们知道Handler往MessageQueue里面添加数据,最终是通过enqueueMessage的函数,那这么多数据往MessageQueue里面去添加,而且发送的小时可能是不同的线程,这就是多线程并发编程。

7.既然可以存在多个Handler往MessageQueue中添加数据(发消息是各个Handler可能处于不同线程),那么内部如何保证线程安全?

通过Handler里面的enqueueMessage函数里面添加发现又这么一个关键字synchronized,将线程锁住了,


message添加

我们再去看Handler里面的取出Message的next函数,发现它也有锁,
message取出

这样添加和取出都添加了锁,就保证了线程的安全性。
这里我们就会有一个疑问,handler 里面有个Dealy的消息,那么它的时间准确性又是怎样的呢?

多线程一旦安全了就不会准确,一旦准确就不安全。多个线程去访问,因为里面添加锁,它需要等待别人处理完了才能到它自己处理,这个时候不管是send还是get都会延迟,所以它的时间准确性是不忘全准确的。
在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景:(图片来自网络)

Synchronized的使用场景

8.我们使用Message是应该如何创建它?

我们在创建Message的时候首先想到的就是new 一个,其实这是不对的,为啥这么说呢,因为我们每次new的时候都是重新去创建一个空间,这样一直创建一直增加内存,会导致内存泄漏,内存抖动,甚至GC。那我们用什么方法呢,Message里面提供一个叫obtain的函数,这个函数有什么优点呢,简单的说就是内存复用,或者说内存共享。



看到obtain的函数实现的方法,它维持了一个Message 池,如果sPool不为空的时候,就可以直接取来message(内存)用,这样就不用不断的new。这个时候我在MessageQueue里面的quit就会发现,它所做的操作只是将消息的内容清空了,然后将消息返回池子里面去了,这个是Message它是一个链表结构,是有头结点和尾结点,我们这个清空内容的Message是添加到头还是尾呢,通过代码就知道原来是添加到链表的表头的。有人就会说了,那这个链表池子会不会越来越大呢,其实是不会的 在代码里面进行最大数的限制

private static final int MAX_POOL_SIZE = 50;

如果超过了50个消息,就不能往这个池子里面添加了。如果链表池子不能再添加了,系统就会及时的回收,将Message置为null


9.使用Handler的postDealy后消息队列会有什么变化?

​ MessageQueue里的消息会以时间顺序(执行的先后顺序)来排序,使用Handler的postDealy后,MessageQueue里的消息会进行重新排序。

/**
     * 发送仅包含what值的消息,该消息将在经过指定的时间后传递。
     * @see #sendMessageDelayed(android.os.Message, long) 
     * 
     * @return 如果消息已成功放入消息队列,则返回true。失败时返回false,通常是因为正在       *  处理消息队列的循环程序正在退出。
     */
    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

如果消息队列为空,这条消息就不会执行,消息不会被发送,而是计算等待的时间,在MessageQueue里的enqueueMessage函数里面一旦添加消息就会,wake唤醒消息队列,然后在next函数里面就会计算我们需要等待的时间,然后离开此次循环,继续for循环,继续睡眠继续等待。

10.Looper死循环为什么不会导致应用卡死?

10.1 既然Handler消息诠释Loop来的,为什么没有ANR问题?

​ 我们常说5s不响应就会出现ANR,那为啥休眠好长时间也不ANR呢?产生ANR的原因不是因为主线程休眠时,而是因为输入事件没有响应,输入事件没有响应就没法唤醒Looper,才加入了五秒限制
​ 应用在没有消息的时候,是在休眠,释放线程,不会导致应用卡死,卡死是ANR,而Looper是睡眠。

子线程发送消息,主线程取出消息

子线程:handler.sendMessage(sendMessageDelayed)-- enqueueMessage--  queue.enqueueMessage(管理内存的)

主线程:ActivityThread 中调用 Looper.loop -- queue.next 取出消息 -- dispatchMessage分发消息 --handler.handlerMessage

我们APP都有一个自己的虚拟机,我们手机上都有一个luncher, Launcher 应用里点击 我们的应用图片,启动 zygote,创建一个JVM,JVM会去调用ActivityThread中的main函数,这也是前面说到的, Looper.prepareMainLooper()和Looper.loop()。所有的activity和server都是通过Looper以消息的方式存在。



在Activity启动时,会走到ActivityThread的main方法

public static void main(String[] args) {
    Looper.prepareMainLooper(); // 创建Looper和MessageQueue对象

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop(); // 循环

    //应用崩溃或者是退出了
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

下面我们来看loop()循环,通过一个for()无限循环来处理消息dispatchMessage(msg), 当MessageQueue中无消息时,会block在queue.next()

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    final MessageQueue queue = me.mQueue;
    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();
    for (;;) { // 这里会无限循环
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        msg.target.dispatchMessage(msg);

        msg.recycleUnchecked();
    }
}

那这里为什么没有导致ANR或者程序死循环呢?

当MessageQueue中无消息时,queue.next()会阻塞在nativePollOnce(),这里涉及到Linux pipe/epoll机制,nativePollOnce()被阻塞时,主线程会释放CPU资源,进入休眠状态. 直到下个消息到达或者有事务发生,会通过pipe管道写入数据来唤醒主线程工作,就可以继续工作了.

这里主线程进入休眠状态和死循环是有区别的.

死循环是指主线程死锁在这里,一直执行某一块代码,无法再响应其他事件.

休眠状态是指在内核状态里,主线程被挂起,线程状态转移到休眠状态.
ActivityThread里面的源码东西实在是太多了,牵涉到好多知识点,例如app的启动原理启动流程,

本人是个手残,一些图片来源于网络。

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