handler机制--让线程变为“永动机”

handler要想能工作起来,第一步要做的事情是让线程变为“永动机”,也就是让线程一直循环起来,不死掉,这样线程就可以不断的处理各种任务了。那这节就来介绍下如何让线程变为“永动机”。

如何让线程变为“永动机”

下面代码可以做到

public class Thread{

    public void run(){
        Looper.prepare();

        Looper.loop();
    }
}

如上代码,需要依次调用Looper.prepare()和Looper.loop()方法就可以让线程变为“永动机”,是不是非常的简单,那我们就从源码角度一趟究竟。

线程变为“永动机”-源码分析

前置知识

fd:Linux系统中把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文件,所有执行 I/O 操作的系统调用都会通过文件描述符,文件描述符不一定是文件,也可以是一块匿名内存。
epoll:io多路复用技术,就是在一个线程或进程中监听多个文件描述符是否可以执行io操作的能力,handler使用了这种技术,在下面会详细介绍
eventfd:类似于管道的概念,可以实现线程间的事件通知 eventfd原文
ThreadLocal:这个类的主要作用是保存当前线程独有的数据。

相关类介绍

下面的文件属于android s

frameworks/base/core/java/android/os/Looper.java
frameworks/base/core/java/android/os/MessageQueue.java
libcore/ojluni/annotations/hiddenapi/java/lang/ThreadLocal.java

frameworks/base/core/jni/android_os_MessageQueue.cpp
system/core/libutils/Looper.cpp

那我们就分别从:Looper.prepare()和Looper.loop()这两个方法作为分析起点来进行分析

1. Looper.prepare()

    public static void prepare() {
        //调用prepare方法,quitAllowed值为true,代表允许结束loop
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        //当前线程已经存在Looper,则不能再次创建,抛异常
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        //sThreadLocal是一个ThreadLocal类型的静态变量,它存储Looper实例
        [1.1]
        sThreadLocal.set(new Looper(quitAllowed));
    }

sThreadLocal.set(new Looper(quitAllowed)) 这行代码的作用是把Looper实例存储在ThreadLocal中,存储下来的一个最大的目的就是:能更方便的获取当前Thread对应的Looper。比如可以在代码的任何位置,只要调用Looper.myLooper()方法就能非常方便的获取到Looper对象。

1.1 Looper#Looper()

    //Looper的构造方法是私有的,只能调用Looper的静态方法来创建Looper
    private Looper(boolean quitAllowed) {
        //new MessageQueue
        [1.2]
        mQueue = new MessageQueue(quitAllowed);
        //保存当前线程引用
        mThread = Thread.currentThread();
    }

1.2 MessageQueue#MessageQueue()

    MessageQueue(boolean quitAllowed) {
        //quitAllowed的值当前为true
        mQuitAllowed = quitAllowed;
        //这一看就是进入nativeInit jni方法,该方法返回一个指针,mPtr保存下来
        [1.3]
        mPtr = nativeInit();
    }

1.3 android_os_MessageQueue.cpp#android_os_MessageQueue_nativeInit

    static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) {
        //new一个NativeMessageQueue
        [1.4]
        NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue();
        if (!nativeMessageQueue) {
            jniThrowRuntimeException(env, "Unable to allocate native queue");
            return 0;
        }

        //增加它的引用计数器
        nativeMessageQueue->incStrong(env);
        //把nativeMessageQueue指针转化为jlong类型,返回给java层,
        return reinterpret_cast<jlong>(nativeMessageQueue);
    }

1.4 android_os_MessageQueue.cpp#NativeMessageQueue

    NativeMessageQueue::NativeMessageQueue() :
            mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
        //先获取当前线程的Looper,不存在则创建,native层也存在一个Looper
        mLooper = Looper::getForThread();
        if (mLooper == NULL) {
            //new 一个Looper出来
            [1.5]
            mLooper = new Looper(false);
            Looper::setForThread(mLooper);
        }
    }

1.5 Looper.cpp#Looper

    Looper::Looper(bool allowNonCallbacks)
        : mAllowNonCallbacks(allowNonCallbacks),
          mSendingMessage(false),
          mPolling(false),
          mEpollRebuildRequired(false),
          mNextRequestSeq(WAKE_EVENT_FD_SEQ + 1),
          mResponseIndex(0),
          mNextMessageUptime(LLONG_MAX) {
        //eventfd方法返回fd,EFD_NONBLOCK的作用在调用read/write函数的时候 不阻塞,EFD_CLOEXEC作用是在fork子进程时候调用exec()方法的时,把fd close调,这样在子进程就不存在相应的fd了
        mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
        LOG_ALWAYS_FATAL_IF(mWakeEventFd.get() < 0, "Could not make wake event fd: %s", strerror(errno));

        AutoMutex _l(mLock);
        rebuildEpollLocked();
    }

该方法中有几个关键点需要介绍下:
eventfd:可以实现线程之间或者进程之间通信,eventfd类似于pipe,但是比 pipe 更高效,一方面它比 pipe 少用一个fd,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部“buffer”一共只有8字节。eventfd在通信时候不能传大数据。
eventfd是如何实现线程之间的通信,通过下面的伪代码来说明

    //用来保持生成的fd
    savedEventfd

    //调用eventfd方法,返回的fd是阻塞类型的
    savedEventfd = eventfd(0, EFD_CLOEXEC);

    //下面方法发生于线程A中,调用read方法从savedEventfd中读取int数据,因为savedEventfd是阻塞类型的,因此线程A会阻塞于read方法,直到有数据为止
    uint64_t counter;
    read(savedEventfd,&counter,sizeof(uint64_t));

    //下面方法调用发生于线程B中,调用write方法往savedEventfd中写一个int值,因为savedEventfd写入了数据,上面线程A就会被唤醒,读出刚刚写入的int数据
    uint64_t inc = 1;
    write(savedEventfd,&inc,sizeof(uint64_t));

mWakeEventFd:它保存了eventfd方法返回的fd,并且需要注意,在调用eventfd方法的时候,传递了EFD_NONBLOCK这个参数,表示返回的fd是非阻塞类型,即调用read,write方法不会发生阻塞,上面的伪代码是阻塞的,现在却是非阻塞类型的,那又怎么实现线程之间或者进程之间通信呢?答案是结合epoll机制。
mWakeEventFd的主要作用是java层的MessageQueue的唤醒/等待操作,唤醒/等待操作都是通过给mWakeEventFd写数据和读数据实现的。

1.6 Looper.cpp#rebuildEpollLocked

    void Looper::rebuildEpollLocked() {
        // Close old epoll instance if we have one.
        // mEpollFd存在,则重新设置
        if (mEpollFd >= 0) {
    #if DEBUG_CALLBACKS
            ALOGD("%p ~ rebuildEpollLocked - rebuilding epoll set", this);
    #endif
            mEpollFd.reset();
        }

        // Allocate the new epoll instance and register the wake pipe.
        //调用epoll_create1方法重新创建,这个方法会返回一个fd并赋值给mEpollFd
        mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
        LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));

        struct epoll_event eventItem;
        memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
        eventItem.events = EPOLLIN;
        eventItem.data.fd = mWakeEventFd.get();
        //通过epoll_ctl方法来添加一个event
        int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);
        LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s",
                            strerror(errno));

        //若mRequests中存在,则依次调用epoll_ctl方法添加event
        for (size_t i = 0; i < mRequests.size(); i++) {
            const Request& request = mRequests.valueAt(i);
            struct epoll_event eventItem;
            request.initEventItem(&eventItem);

            int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, request.fd, &eventItem);
            if (epollResult < 0) {
                ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s",
                      request.fd, strerror(errno));
            }
        }
    }

在介绍上面方法之前,先来介绍下fd管道,管道,eventfd,再来介绍epoll机制。

管道,FIFO,fd管道,eventfd
这节不是介绍handler机制吗?怎么涉及到了管道这些内容,主要原因是咱们的handler机制已经不是单单解决线程之间通信的问题,还解决进程之间的通信。

管道:是半双工的(就是数据只能在一个方向上流动),只能有公共祖先的两个进程之间使用
FIFO:也是一种管道,它没有管道的只能在公共祖先的两个进程之间使用的限制
fd管道:又称UNIX 域套接字,它是全双工的(一端既可以是写端也可以是读端),并且对进程没有限制。vsync机制就使用的是fd管道
eventfd:上面已经介绍了,它的优点高效/少占用资源,缺点:传播的数据只能是int。

这四者都可以实现线程之间/进程之间的通信,并且它们的read,write的目标都是fd(文件描述符)。它们实现通信的方式如上面eventfd的伪代码一样,在创建的时候需要设置为阻塞模式,在阻塞模式下read,write函数都是阻塞的,因此尤其read函数的调用就需要放在单独的线程中。那如果创建了很多个管道来实现线程之间通信,那岂不是要创建很多的读线程,随着管道的数量多起来,读线程也增加,这种方式肯定不是一个好的解决方案,解决这个问题的一个技术是IO多路复用。

epoll
IO多路复用:我的理解是比如原先创建一个阻塞型的管道,那就需要创建一个读线程专门的来监听管道的读端是否有数据,那创建n多个阻塞型管道,那就需要创建n多个读线程;那IO多路复用就是只创建一个读线程,来监听n多个管道读端的数据

IO多路复用的优势是不是很明显,它有多种实现:select机制,poll机制,epoll机制。epoll机制是最高效,优点最多的一个机制,用一段伪代码来介绍下它的使用:

    //1. 调用epoll_create1方法先创建,该方法返回一个fd
    epollFd = epoll_create1(EPOLL_CLOEXEC);

    //2. 调用epoll_ctl方法添加一个event,epoll_ctl方法的第一个参数就是epollFd。这样event就和epollFd绑定在了一起,epollFd就可以监听event上的数据了
    struct epoll_event eventItem;
    //event类型
    eventItem.events = EPOLLIN;
    //event对应的fd
    eventItem.data.fd = pipeFd;
    int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);

    //3. 调用epoll_wait方法开始监听所有event的数据。在没有监听到数据的情况下,会进入阻塞状态,会释放cpu等资源。参数timeoutMillis代表等待时间,== 0代表不等待立马返回,== -1 则代表等待,直到等待数据为止,> 0则代表需要等待的时间。
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

总结下epoll的用法:

  1. 初始化,调用epoll_create1方法创建并返回一个fd
  2. 添加event事件,调用epoll_ctl方法
  3. 等待数据到来,调用epoll_wait,它的参数timeoutMillis代表等待时间,== 0代表不等待立马返回,== -1 则代表等待,直到等待数据为止,> 0则代表需要等待的时间。在没有数据到来的时候,会进入阻塞状态,会释放cpu等资源

rebuildEpollLocked
那就来看下rebuildEpollLocked方法所做的事情:

  1. 调用epoll_create1方法创建并返回fd赋值给mEpollFd
  2. 把mWakeEventFd关联的epoll_event,调用epoll_ctl方法添加该事件,这样mEpollFd就和mWakeEventFd产生了关联,就可以监听它的数据了
  3. 把mRequests中的请求也通过epoll_ctl方法添加这些事件。mRequests包含了各种需要监听的fd,比如:vsync机制就是通过fd管道实现的,其中一个fd在surfaceflinger进程,另外一个对应的fd位于app进程,位于app进程的fd会通过Looper.cpp#addFd方法把自己加入mRequests,这样就可以通过epoll来监听是否有数据到来

1.7 小结

到此Looper.prepare()方法的流程就分析完了,用一张时序图来看下整个调用流程


handler-looper-prepare

2.Looper.loop()

再来分析下Looper.loop()这个方法的流程

    public static void loop() {
        //获取线程绑定的Looper
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        if (me.mInLoop) {
            Slog.w(TAG, "Loop again would have the queued messages be executed"
                    + " before this one completed.");
        }

        me.mInLoop = true;

        省略代码......

        //启动一个死循环
        for (;;) {
            [2.1]
            if (!loopOnce(me, ident, thresholdOverride)) {
                return;
            }
        }
    }

    public static @Nullable Looper myLooper() {
        //从ThreadLocal中获取Looper
        return sThreadLocal.get();
    }

2.1 Looper.loopOnce

    private static boolean loopOnce(final Looper me,
            final long ident, final int thresholdOverride) {
        //从MessageQueue中取Message,若没有可执行的Message 则block
        [2.2]
        Message msg = me.mQueue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return false;
        }

        省略代码......

        try {
            msg.target.dispatchMessage(msg);
            if (observer != null) {
                observer.messageDispatched(token, msg);
            }
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } catch (Exception exception) {
            if (observer != null) {
                observer.dispatchingThrewException(token, msg, exception);
            }
            throw exception;
        } finally {
            ThreadLocalWorkSource.restore(origWorkSource);
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        
        省略代码......

        msg.recycleUnchecked();

        return true;
    }

me.mQueue.next()会进入MessageQueue.next()方法获取Message,进入该方法

2.2 MessageQueue.next

    Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;

        //同样起一个死循环
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            //进入jni方法, 这时候nextPollTimeoutMillis的值是-1
            [2.3]
            nativePollOnce(ptr, nextPollTimeoutMillis);

            省略获取Message的代码...... (下一节会重点介绍)
        }
    }

nativePollOnce方法最终会调jni的android_os_MessageQueue_nativePollOnce方法

2.3 android_os_MessageQueue.cpp#android_os_MessageQueue_nativePollOnce

    //obj java层的MessageQueue, ptr NativeMessageQueue指针,timeoutMillis值为-1
    static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
            jlong ptr, jint timeoutMillis) {
        //把ptr转换为NativeMessageQueue指针
        NativeMessageQueue* nativeMessageQueue = reinterpret_cast<NativeMessageQueue*>(ptr);
        [2.4]
        nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
    }

2.4 android_os_MessageQueue.cpp#nativeMessageQueue#pollOnce

    void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
        mPollEnv = env;
        mPollObj = pollObj;
        //调用pollonce方法,timeoutMillis值为-1
        [2.5]
        mLooper->pollOnce(timeoutMillis);
        mPollObj = NULL;
        mPollEnv = NULL;

        if (mExceptionObj) {
            env->Throw(mExceptionObj);
            env->DeleteLocalRef(mExceptionObj);
            mExceptionObj = NULL;
        }
    }

2.5 Looper.cpp#pollOnce

    //下面方法在system/core/libutils/include/utils/Looper.h
    int pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData);
    inline int pollOnce(int timeoutMillis) {
        //调用了pollOnce的重载方法,outFd,outEvents,outData都为nullptr
        return pollOnce(timeoutMillis, nullptr, nullptr, nullptr);
    }


    //下面方法在system/core/libutils/Looper.cpp
    //timeoutMillis为-1
    int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
        int result = 0;
        //同样死循环
        for (;;) {
            //若有没处理的response,则处理,并返回
            while (mResponseIndex < mResponses.size()) {
                const Response& response = mResponses.itemAt(mResponseIndex++);
                int ident = response.request.ident;
                if (ident >= 0) {
                    int fd = response.request.fd;
                    int events = response.events;
                    void* data = response.request.data;
    #if DEBUG_POLL_AND_WAKE
                    ALOGD("%p ~ pollOnce - returning signalled identifier %d: "
                            "fd=%d, events=0x%x, data=%p",
                            this, ident, fd, events, data);
    #endif
                    if (outFd != nullptr) *outFd = fd;
                    if (outEvents != nullptr) *outEvents = events;
                    if (outData != nullptr) *outData = data;
                    return ident;
                }
            }

            //若result不为0,则返回,刚开始进入这方法,result是0
            if (result != 0) {
    #if DEBUG_POLL_AND_WAKE
                ALOGD("%p ~ pollOnce - returning result %d", this, result);
    #endif
                if (outFd != nullptr) *outFd = 0;
                if (outEvents != nullptr) *outEvents = 0;
                if (outData != nullptr) *outData = nullptr;
                return result;
            }
            //进入pollInner方法
            [2.6]
            result = pollInner(timeoutMillis);
        }
    }

2.6 Looper.cpp#pollInner

    //timeoutMillis值为-1
    int Looper::pollInner(int timeoutMillis) {
    #if DEBUG_POLL_AND_WAKE
        ALOGD("%p ~ pollOnce - waiting: timeoutMillis=%d", this, timeoutMillis);
    #endif

        // Adjust the timeout based on when the next message is due.
        //因为timeoutMillis当前值为-1 并且 mNextMessageUptime在Looper构造方法初始化的时候,它的值为LLONG_MAX,因此不会进入下面调整timeout的逻辑
        if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
            int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
            if (messageTimeoutMillis >= 0
                    && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
                timeoutMillis = messageTimeoutMillis;
            }
    #if DEBUG_POLL_AND_WAKE
            ALOGD("%p ~ pollOnce - next message in %" PRId64 "ns, adjusted timeout: timeoutMillis=%d",
                    this, mNextMessageUptime - now, timeoutMillis);
    #endif
        }

        // Poll.
        int result = POLL_WAKE;
        mResponses.clear();
        mResponseIndex = 0;

        // We are about to idle.
        mPolling = true;

        //定义eventItems数组它主要接受传递过来的event, 调用epoll_wait方法开始等待event,因为timeoutMillis当前的值为-1,因此会阻塞等待events,并且释放cpu等资源
        struct epoll_event eventItems[EPOLL_MAX_EVENTS];
        int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

        省略掉处理wakefd和mRequest的代码......(后面章节会详细讲解)

        return result;
    }

该方法会调用epoll_wait方法等待events,因为timeoutMillis当前的值为-1,因此会使当前线程进入阻塞状态,并释放cpu等资源

2.7 小结

到此 Looper.loop()方法的流程就分析完了,用一张时序图总结下:


handler-looper-loop

总结

让线程变为“永动机”可以分为两个步骤:

  1. 准备阶段,Looper.prepare()方法其实做的都是准备工作:

    • 初始化Looper对象,并且把他放入ThreadLocal中,放入ThreadLocal的主要作用就是:为了能在当前线程的任何代码处非常方便的获取到当前线程”绑定“的Looper。
    • 初始化MessageQueue对象,它与上面初始化的Looper对象是一对一关系
    • 初始化native层的Looper对象,调用eventfd方法创建fd;调用epoll_create方法创建epollfd,并且依次调用epoll_ctl把mWakeEventfd以及mRequest封装成event并添加,这样就可以通过epoll机制来监听它所添加的event上面的事件是否发生了
  2. 开始工作阶段,在线程中调用Looper.loop()方法后就开始工作了:

    • Looper的loop方法会启动一个死循环,这样一个线程就真正的变成“永动机”了
    • 启动死循环后,会调用MessageQueue的next方法从中获取Message,next方法调用了nativePollOnce方法,最终会调用到Looper.cpp的pollInner方法,由于第一步准备阶段,epoll,eventfd相关的准备工作都已经准备好了,pollInner方法中会调用epoll_wait方法等待事件到来,因为这时候的等待时间(timeoutMillis)为-1,会一直等待,直到有事件发生为止,进而导致当前的线程进入阻塞状态,并释放cpu等资源

因为handler使用了epoll机制,handler既可以实现线程之间通信,也可以实现进程之间通信。

好了,到此线程已经做好了一切准备,就等待着“各种事件“的到来了。

思考

MessageQueue的next方法获取消息时候 等待/唤醒 实现方案为啥没用 wait/notify 来实现?
上面源码分析提到MessageQueue的next方法最终是因为epoll_wait方法,导致线程进入 等待阻塞状态的,那为啥没有使用wait/notify来实现呢?大家其实可以找很早以前的android代码,那时候确实是用wait/notify来实现 等待/唤醒机制。

我认为的主要原因是:用epoll机制实现的 等待/唤醒机制,主要是它有如下优点:

  • 功能强大:不仅实现线程之间通信,还实现进程之间通信的功能,vsync机制就是利用fd管道实现进程通信,epoll只需要监听fd管道的一端fd上的数据状态即可,surfaceflinger进程往对端fd上写数据,epoll在当前的线程中就可以监听到surfaceflinger发过来的数据。
  • 高性能: epoll机制可以监听n多个fd,并且不会随着fd的增加而性能下降
  • 扩展性好: 只需要调用Looper.cpp的addFd方法就可以在当前线程监听fd上的数据
  • 既可以为java层提供服务,也可以为native层提供服务:native层的Looper.cpp类其实也提供了和上层MessageQueue相关的功能

假如使用wait/notify实现进程之间通信就困难了,并且即使实现了还可能会涉及到线程之间的切换,性能方面肯定大打折扣。

为啥要用eventfd机制?
在把Message放入MessageQueue的时候,这时候只是需要给阻塞的MessageQueue发一个有多简单就能多简单的通知或者信号就行,告诉它有消息到达因。为没有用 wait/notify 来实现 等待/唤醒,所以就需要用管道这类技术来实现,但是用管道做这种事情又大才小用了,evenfd是最合适,它占用的内存非常小并且只使用一个fd,并且它就是发送一个int类型的值就可以,因此使用了eventfd来实现:把Message放入MessageQueue的时候,通知阻塞的MessageQueue有消息到来了这样的功能。

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

推荐阅读更多精彩内容