Handler

带着以下几个问题去看Handler源码

  • 1、Handler与线程的关系;
  • 2、Handler、Looper、MessageQueue的关系;
  • 3、Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
  • 4、Activity的生命周期这些方法这些都是在主线程里执行的吧,那这些生命周期方法是怎么实现在死循环体外能够执行起来的?
  • 5、postDelay时, 任务是如何被添加到消息队列中的?

打算按下面顺序进行分析Looper体系:

  • 1、创建轮询器new Looper
  • 2、创建消息队列new MessageQueue
  • 3、启动轮询器进行消息查询Looper.loop
  • 4、通过Handler.post/sendMessage向消息队列中添加msg

一、轮询器Looper

1.1 ActivityThread.main主线程Looper的创建入口
static Handler sMainThreadHandler;
// ActivityThread.main是主程序的入口, 在这里进行Looper与Message的初始化操作;
public static void main(String[] args) {
    // 创建轮询器Looper
    Looper.prepareMainLooper();      
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    // 启动轮询器
    Looper.loop();
}

小结:main里面要计划完成三件事: 创建轮询器Looper、创建Handler、启动轮询器Looper.

1.2 Looper.prepareMainLooper创建轮询器Looper与消息列队
public static void prepareMainLooper() {
    // 创建Looper, 此时Looper关联的是UI线程
    prepare(false);
    synchronized (Looper.class) {
        // 这里限制了一个线程只能有一个Looper
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        // 对sMainLooper进行赋值
        sMainLooper = myLooper();        
    }
}
private static void prepare(boolean quitAllowed) {
    // Looper与Thread是1:1的关系
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    // 初始化Looper, 将Looper通过ThreadLocal与当前线程进行绑定
    sThreadLocal.set(new Looper(quitAllowed));
}

public static void prepare() {
    prepare(true);
}
private Looper(boolean quitAllowed) {
    // 一个线程对应一个Looper, Looper初始化时进行了MessageQueue的初始化, 
    // 所以Thread、Looper、MessageQueue三者关系为1:1:1
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

小结: 上面流程完成之后, 代码层面规定了Thread、Looper、MessageQueue三者的关系是1:1:1, 但是还有一个疑问是quitAllowed这个变量, prepareMainLooper()与prepare()分别传入的false/true, 这个变量对Looper体系有什么影响?

二、消息队列MessageQueue

2.1 MessageQueue构造函数
private native static long nativeInit();
// java层的MessageQueue持有native层的NativeMessageQueue的引用;
private long mPtr;
MessageQueue(boolean quitAllowed) {
    mQuitAllowed = quitAllowed;
    // 触发native层方法, 获取native层的NativeMessageQueue的引用, 使其指向mPtr;
    mPtr = nativeInit(); 
}

http://androidxref.com/7.1.1_r6/xref/frameworks/base/core/jni/android_os_MessageQueue.cpp;

2.2 MessageQueue.nativeInit
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
    NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();  
    nativeMessageQueue->incStrong(env);
    return reinterpret_cast<jlong>(nativeMessageQueue);
}

小结: native创建NativeMessageQueue对象, 并将该对象的引用返回给java层MessageQueue的mPtr变量, 从而将Java层MessageQueue与NativeNativeMessageQueue产生了关联, 为后续Looper没有消息到来时进入阻塞状态打下基础

2.3 NativeMessageQueue构造函数
NativeMessageQueue::NativeMessageQueue() :
        mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
    mLooper = Looper::getForThread();
    if (mLooper == NULL) {
        // 创建Native层的Looper, 然后将Native层的Looper与当前线程进行绑定
        mLooper = new Looper(false);   
        Looper::setForThread(mLooper);
    }
}

小结:创建Native层的Looper, 将Looper对当前线程进行绑定, 这里也采用的是单例的模式, 所以Native层Looper、NativeMessageQueue、三者的关系也是1:1:1

三、启动轮询器

3.1 Looper.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;
    for (;;) {
        // 轮询器从消息队列中获取消息, 然后执行该消息.
        Message msg = queue.next(); 
        // 如果msg为null, 也就是说消息队列中没有消息了, 那么轮询器退出当前轮询操作, 加入当前msg为null, 
        // 然后return退出当前轮询操作, 那么别的线程再向往消息队列中添加信息时岂不是每次都需要调用loop()?
        // 其实通过下文中的queue.next()发现并不会出现msg = null的情况
        if (msg == null) {
            return;
        }
        // 取消消息以后, 然后执行该消息
        msg.target.dispatchMessage(msg);
        msg.recycleUnchecked();
    }
}

小结:
1、轮询器looper的轮询操作是在一个while(true)循环中执行的, 从代码层面看, 退出while(true)的条件是msg = null, 然后考虑一个问题, msg到底会不会存在=null的情况?
2、取出消息, msg不为null的情况下, 执行该msg.

3.2 执行消息msg.target.dispatchMessage
// 1. post方式时callback == Runnable != null, 触发handlerCallback();
// 2. sendMessage方式时callback == null, 触发handleMessage;
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

小结:从消息队列中取出消息是进行消息分发处理, 这里分发的依据是Handler发送消息的方式, Handler发送消息主要有两种方式: post与sendMessage, 如果是post方式, 则执行handleCallback方法, 如果是sendMessage方式, 则执行handleMessage方法

3.3 从消息队列中读取消息MessageQueue.next
private native void nativePollOnce(long ptr, int timeoutMillis);
Message next() {
    int pendingIdleHandlerCount = -1;
    // nextPollTimeoutMillis默认为0;
    int nextPollTimeoutMillis = 0;
    for (;;) {
        // 记住这个方法是一个native的方法, 他在native层到底做了什么操作?
        // 通过对下面代码的分析可知, nextPollTimeoutMillis有三种值:
        // (1) nextPollTimeoutMillis = 0: 消息队列中由消息;
        // (2) nextPollTimeoutMillis = Math.min(msg.when - now, Integer.MAX_VALUE): 延时消息;
        // (3) nextPollTimeoutMillis = -1: 当前消息队列中没有消息;
        nativePollOnce(ptr, nextPollTimeoutMillis);     
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            // 1.通过查看Message的源码, Message是一个链表结构;
            // 2.这里先记住结论, Handler.enqueueMessage时会将Message与Handler进行绑定, 
            //   所以正常情况下msg.target != null;
            if (msg != null && msg.target == null) {
                do {
                    prevMsg = msg;
                    msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());
            }
            if (msg != null) {
                // 如果now < msg.when, 说明当前消息为延时消息, 然后跳过该if语句继续向下执行
                if (now < msg.when) {
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {
                    // 
                    mBlocked = false;
                    // 从链表中FIFO的方式依次取出消息, 然后从链表中删除该元素
                    if (prevMsg != null) {
                        prevMsg.next = msg.next;
                    } else {
                        mMessages = msg.next;
                    }
                    msg.next = null;
                    msg.markInUse();
                    // 取出msg返回msg
                    return msg;
                }
            } else {
                // 当msg = null时, nextPollTimeoutMillis = -1.
                nextPollTimeoutMillis = -1;
            }
            // 执行接下来的代码有两种情况:
            // 1. msg != null, 但是now < msg.when, 也就是说当前消息为延时消息, 
            //    此时nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE)
            // 2. msg == null, 也就是说此时消息队列中没有消息, 此时nextPollTimeoutMillis = -1
            if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();
            }
            if (pendingIdleHandlerCount <= 0) {
                // 再次执行for循环操作
                mBlocked = true;
                continue;
            }
        }
        nextPollTimeoutMillis = 0;
    }
}

小结:重点在nextPollTimeoutMillis取值上面.
1、nextPollTimeoutMillis = 0: 消息队列中由消息;
2、nextPollTimeoutMillis = Math.min(msg.when - now, Integer.MAX_VALUE): 延时消息;
3、nextPollTimeoutMillis = -1: 当前消息队列中没有消息;

3.4 MessageQueue.nativePollOnce
MessageQueue->nativePollOnce:
static void android_os_MessageQueue_nativePollOnce(...jlong ptr, jint timeoutMillis) {
    // 结合上文可知ptr指向的就是NativeMessageQueue对象
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

NativeMessageQueue->pollOnce():
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    mLooper->pollOnce(timeoutMillis);   
}
3.5 Looper.pollOnce
Looper->pollOnce(...):
// timeoutMillis为我们在java中传入的nextPollTimeoutMillis变量;
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        while (mResponseIndex < mResponses.size()) {...}
        if (result != 0) {...}
        result = pollInner(timeoutMillis);
    }
}

int Looper::pollInner(int timeoutMillis) {
    // 只观察timeoutMillis引用的地方;
    // 由上文Native层的Looper初始化知道, mNextMessageUptime = LLONG_MAX;
    if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {...}
    int result = POLL_WAKE;
    mResponses.clear();
    mResponseIndex = 0;
    mPolling = true;
    // 1. 当timeoutMillis = -1时, 由epoll_wait定义可知该方法会一直阻塞;
    // 2. 如果timeoutMillis = 0继续向下执行, 并获取事件个数;
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    // epoll_wait是一个Linux函数, 这里只记住结论.
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); 
    /**
     * 当eventCount > 0 时, 遍历该eventItem, 并判断当前文件描述符是否为
     * mWakeEventFd, 如果是mWakeEventFd则调用awoken()唤醒, 否则则将该文件描述符
     * 添加到mResponses中去;
     */
    for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeEventFd) {
            if (epollEvents & EPOLLIN) {
                awoken();
            } 
        } else {
            ssize_t requestIndex = mRequests.indexOfKey(fd);
            if (requestIndex >= 0) {
                int events = 0;
                if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
                if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
                if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
                if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
                pushResponse(events, mRequests.valueAt(requestIndex));
            } 
            else {...}
        }
    }
    return result;
}
3.6 epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):

小结:
1、timeout = 0: 立即返回;
2、timeout = -1: 永久阻塞当前线程;
3、timeout > 0: 当前线程阻塞timeout时间后继续向下执行;
总结:如果消息队列为空, java层nextPollTimeoutMillis = -1, 对应native层timeout = -1, 此时当前线程会进入阻塞状态, 也就是Looper所在线程会被挂起.

四、Handler

4.1 Handler构造函数
public Handler() {
    this(null, false);
}
public Handler(Callback callback, boolean async) {
    // 1. 结合前文其实可知, Looper的获取使用了ThreadLocal, 也就是说一个线程只能有一个Looper;
    // 2. 但是Handler初始化时并没有做相关限制, 所以Handler与Looper的关系是多:1的关系;
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
}

小结:初始化Handler之前, 需要先创建一个轮询器Looper.

4.2 提交消息Handler.post
public final boolean post(Runnable r) {
    // 1. 将Runnable封装进Message中, 然后将Message发送到MessageQueue;
    // 2. 如果是发送延时消息, 第二个参数delayMillis是不为0的;
    return sendMessageDelayed(getPostMessage(r), 0);   
}
private static Message getPostMessage(Runnable r) {
    // 初始化Message, 将r指向Message.callback;
    Message m = Message.obtain();
    m.callback = r;
    return m;
}
public final class Message {
    public static Message obtain() {
        return new Message();
    }
}
4.3 遍历消息链表, 选择合适的位置插入msgHandler.sendMessageDelayed
public final boolean sendMessageDelayed(Message msg, long delayMillis) {
    // 将当前时间与延时时间进行叠加获取消息处理的时间.
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    // msg进行入队操作;
    return queue.enqueueMessage(msg, uptimeMillis);  
}
4.4 其他线程发送消息唤醒MessageQueueMessageQueue.enqueueMessage
boolean enqueueMessage(Message msg, long when) {
    ...
    synchronized (this) {
        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        // 1. 进入if的三种情况:
        //   (1) 当前消息队列中没有消息;
        //   (2) 提前执行任务, 也就是说SystemClock.uptimeMillis() + delayMillis = 0?
        //   (3) when < p.when, 新插入的消息在当前消息之前执行, 也就是会插入到表头的位置;
        // 2. 结合上文MessageQueue.next可知, 当消息队列为空时, Looper所在线程会在nativePollOnce处进入
        //    挂起状态, 同时mBlocked被置为true, 知道外部线程通过I/O方式唤醒Looper线程.
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            // mMessages相当于是链表的表头, 此时将当前消息置为表头的位置
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // 此时很显然p.target == null为false, neekWake = false;
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            // 关于开头提出的问题, 延时消息的插入是在插入时进行延时, 还是在取出时进行延时?
            // 遍历链表按执行时间的先后顺序进行插入msg.
            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;
        }
        // 1. 能进入if语句内部的前提条件是needWake = true, 而needWake唯一被赋值就是needWake = mBlocked;
        // 2. mBlocked只在MessageQueue.next()中被赋值.而结合上文又知道当没有消息被添加到MessageQueue队列中时, 
        //    也就是Message链表为空时,  MessageQueue.next()会被一直阻塞在nativePollOnce(...), 而且mBlocked = true, 
        //    当有消息被添加到消息队列时, 是一定会执行下面if(...)语句;
        if (needWake) {
            // 1. 通过模块<3.3>可知, 当没有消息时, 当前线程会在native层通过触发epoll_wait方法并传入
            //    -1将当前线程处于阻塞状态;
            // 2. 然后在有消息到来时, 通过nativeWake方法欢喜线程;
            nativeWake(mPtr);     
        }
        return true;
    }
}
private native static void nativeWake(long ptr);

小结:
1、当没有消息到来时, MessageQueue.next()会被一直阻塞在nativePollOnce(...)处, 并且此时mBlock = true;
2、当调用Handler.post或者Handler.sendMessage时, MessageQueue.enqueueMessage(...)会触发nativeWake(...)的执行;
3、插入消息时, 遍历当前消息链表, 插入msg时满足的条件是when从小到大的顺序, 也就是msg执行的先后顺序进行msg的插入操作;

4.5 android_os_MessageQueue.nativeWake
MessageQueue --->
static void android_os_MessageQueue_nativeWake(JNIEnv* env, jclass clazz, jlong ptr) {
    NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
    nativeMessageQueue->wake();
}

void NativeMessageQueue::wake() {
    mLooper->wake();      
}
4.6 Looper.wake
Looper --->
void Looper::wake() {
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
}

  重点在这里的write(...)部分, 注意前文的epoll_wait(...)传入-1时会阻塞当前线程, 这里调用write(...)通过唤醒当前线程, pollOnce(...)会继续向下执行, 最终走到awoken方法;

Looper.cpp --->
void Looper::awoken() {
    uint64_t counter;
    TEMP_FAILURE_RETRY(read(mWakeEventFd, &counter, sizeof(uint64_t)));
}

  从管道内读入数据, 即native层不会再阻塞在pollOnce(...), 所以java层的MessageQueue.next()也就不会再阻塞在nativePollOnce(ptr, nextPollTimeoutMillis)处, MessageQueue.next()继续向下执行由执行到了return msg处, 然后根据我们Handler.post还是Handler.sendMsg而选择是调用Handler.handlerCallback(...)还是Handler.handleMessage(...);

4.7 Handler.dispatchMessage
// 1. post方式时callback == Runnable != null, 触发handlerCallback();
// 2. sendMessage方式时callback == null, 触发handleMessage;
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
总结一下:

  1、关于IO多路复用, epoll以及pipe并不是很了解, 导致分析经常卡壳, 到现在也不能说清楚;

多路复用_epoll:

1、概念:
  epoll使用一个文件描述符管理多个描述符, 将用户关系的文件描述符的事件存放到内核的一个事件表中, 这样在用户空间和内核空间的copy只需要一次;
2、epoll接口:

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

(1) int epoll_create(int size):
  创建一个epoll句柄, size用来告诉内核这个监听的数目一共有多大. 当创建好epoll句柄后, 它就是会占用一个fd值, 在使用完epoll之后, 必须调用close()关闭, 否则就可能导致fd被耗尽;

(2) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):

  epoll的事件注册函数; 第一个参数是epoll_create()的返回值, 第二个参数表示动作, 用三个宏来表示:

EPOLL_CTL_ADD: 注册新的fd到epfd中;
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;

第三个参数是需要监听的fd, 第四个参数是告诉内核需要监听什么事件;

struct epoll_event {
    __uint32_t events;  /* Epoll events */
    epoll_data_t data;  /* User data variable */
};

events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):

  等待事件的产生, 参数events用来从内核得到事件的集合, maxevents告诉内核这个events有多大, 参数timeout是超时时间(毫秒, 0会立即返回, -1将会阻塞). 该函数返回需要处理的事件数目, 如返回0表示已超时;

参考文章:
https://www.cnblogs.com/Anker/p/3263780.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容