Android消息機制,從Java層到Native層剖析

由Handler鸽扁、MessageQueue道逗、Looper構(gòu)成的線程消息通信機制在Android開發(fā)中非常常用,不過大部分人都只粗淺地看了Java層的實現(xiàn)献烦,對其中的細節(jié)不甚了了,這篇博文將研究Android消息機制從Java層到Native層的實現(xiàn)卖词。

消息機制由于更貼近抽象設(shè)計巩那,所以整個結(jié)構(gòu)更簡單,只包含了消息的產(chǎn)生此蜈、分發(fā)即横,不像Input子系統(tǒng)那樣還有歸類、過濾等環(huán)節(jié)裆赵。整體的結(jié)構(gòu)如下圖:

Android Java層消息機制

消息的產(chǎn)生

在Java層中消息的產(chǎn)生都來源于用戶創(chuàng)建的Message對象东囚,經(jīng)過封裝的Runnable對象,或調(diào)用obtainMessage從Message Pool中獲得战授,Message Pool指的是Message類內(nèi)的Message循環(huán)隊列页藻,隊頭是靜態(tài)的Message對象sPool,該隊列最大容納MAX_POOL_SIZE(50)個Message:

MessagePool對Message的復(fù)用節(jié)省了不斷創(chuàng)建Message帶來的開銷植兰,如果當前50個Message都已經(jīng)被用過份帐,由于MessagePool是循環(huán)隊列,則會回到隊頭并請空該Message楣导,向下復(fù)用废境。

BlockingRunnable

看Java層Handler的源碼的時候發(fā)現(xiàn)了一個奇怪的東西:BlockingRunnable,基本上沒有用過的東西筒繁,也沒看別人講過噩凹,于是我就來鉆研一下吧:

private static final class BlockingRunnable implements Runnable {
    private final Runnable mTask;
    private boolean mDone;

    public BlockingRunnable(Runnable task) {
        mTask = task;
    }

    @Override
    public void run() {
        try {
            mTask.run();
        } finally {
            synchronized (this) {
                mDone = true;
                notifyAll();
            }
        }
    }

    public boolean postAndWait(Handler handler, long timeout) {
        if (!handler.post(this)) {
            return false;
        }

        synchronized (this) {
            if (timeout > 0) {
                final long expirationTime = SystemClock.uptimeMillis() + timeout;
                while (!mDone) {
                    long delay = expirationTime - SystemClock.uptimeMillis();
                    if (delay <= 0) {
                        return false; // timeout
                    }
                    try {
                        wait(delay);
                    } catch (InterruptedException ex) {
                    }
                }
            } else {
                while (!mDone) {
                    try {
                        wait();
                    } catch (InterruptedException ex) {
                    }
                }
            }
        }
        return true;
    }
}

我們可以看到,BlockingRunnable是一個“包裹”構(gòu)造方法中傳入的Runnable的Runnable毡咏,調(diào)用BlockingRunnable的postAndWait會做以下事情:

  1. 如果投遞BlockingRunnable失敗驮宴,返回false
  2. 鎖住投遞BlockingRunnable的線程
  3. 如果timeout大于0,計算參數(shù)Runnable的到期時間呕缭,只要參數(shù)Runnable還沒處理完幻赚,則一直輪詢還剩多少時間禀忆,并調(diào)用wait(delay)讓投遞BlockingRunnable的線程繼續(xù)等待,直參數(shù)Runnable處理完(mDone為true)這個過程才結(jié)束
  4. 如果timeout小于等于0落恼,而且參數(shù)Runnable還沒處理完箩退,則一直等待直到參數(shù)Runnable處理完(mDone為true)

這個東西的說明書和使用風險可以在runWithScissors方法的注釋里看到,我在這里就不當翻譯工了佳谦。

消息的投遞和處理

得到Message后戴涝,就會通過Handler的sendMessageAtTime調(diào)用MessageQueue的enqueueMessage將Message投遞到MessageQueue中,在往下學習之前必須先了解Handler的創(chuàng)建钻蔑,因為后面的知識和它有關(guān)聯(lián)啥刻。

Handler的創(chuàng)建和初始化

其實Handler的初始化沒什么好看的,就是保存Callback咪笑、mLooper的MessageQueue的引用可帽,以及聲明Handler是否異步投遞所有Message。但是里面有一個內(nèi)存泄露的檢查窗怒,可以學習一下映跟,就是如果打開了FIND_POTENTIAL_LEAKS,就會進行內(nèi)存泄露的檢查扬虚,它會做以下事情:

  1. 獲取當前Handler類
  2. 如果Handler是匿名內(nèi)部類努隙,或成員類,或局部類辜昵,且Handler的修飾符不是static
  3. 那么就會打出log提示可能會發(fā)生內(nèi)存泄露
public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

既然Handler的創(chuàng)建這么簡單荸镊,為什么說后面要學習的內(nèi)容和它相關(guān)呢?原因就出在Looper中堪置,我們可以看到Looper是通過sThreadLocal返回的钻趋,這個ThreadLocal是什么呢剖毯?

ThreadLocal - 維持線程內(nèi)對象的唯一性

ThreadLocal是一個關(guān)于創(chuàng)建線程局部變量的類列吼。

通常情況下琢融,我們創(chuàng)建的變量是可以被任何一個線程訪問并修改的。而使用ThreadLocal創(chuàng)建的變量只能被當前線程訪問雁竞,其他線程則無法訪問和修改钦椭。它的實現(xiàn)原理如下:

如圖所示,ThreadLocalRef其實是同一個ThreadLocal對象的引用碑诉,為了不讓線看起來很亂我分別用了兩個方塊表示ThreadLocal對象彪腔,但其實是同一個對象。ThreadLocal同時是ThreadA进栽、ThreadB甚至ThreadN內(nèi)ThreadLocalMap的Key德挣,但取出來的對象時不一樣的,因為Map不一樣對應(yīng)的鍵值對也不一樣嘛快毛。

ThreadLocalMap

ThreadLocalMap是僅用于維護ThreadLocal值的自定義HashMap格嗅,只在Thread類內(nèi)使用番挺。為了避免ThreadLocalMap的Key->ThreadLocal在GC時無法被回收,里邊的元素都是用WeakReference封裝的屯掖。ThreadLocalMap除了這點以外玄柏,沒有什么特別的,就不細講了贴铜。

需要注意的一點是:ThreadLocalMap是可能帶來內(nèi)存泄露的粪摘,但root cause不是ThreadLocalMap本身,而是代碼質(zhì)量不夠高绍坝。首先徘意,由于作為Map的Key的ThreadLocal是弱引用,那么GC時ThreadLocal會被回收轩褐,此時Map內(nèi)存在一對Key為null的鍵值對椎咧,而Value仍然被線程強引用著,那么如果用完ThreadLocal后不主動移除把介,就會內(nèi)存泄露了勤讽。但事實上,ThreadLocal用完后主動調(diào)remove就能規(guī)避這個問題劳澄,本來也該這樣做。

Entry

Entry作為ThreadLocalMap的元素蜈七,表示的是一對鍵值對:ThreadLocal的弱引用為鍵秒拔,將要用ThreadLocal存儲的對象為值。

static class Entry extends WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocal總結(jié)

換句話說飒硅,所謂的不可被其他線程修改的局部變量砂缩,表示的是:每個線程中都會維護一個ThreadLocalMap,里邊以ThreadLocal為鍵三娩,對應(yīng)的局部變量為值庵芭,通過鍵值對來控制訪問和數(shù)據(jù)的一致性,而不是通過鎖來控制雀监。

Looper

既然一個線程只有一個Looper双吆,那么Looper里面有什么呢?從源碼可以看到会前,Looper的構(gòu)造方法是私有的好乐,也就意味著獲得Looper對象基本都是單例,這一點和線程<->Looper的一對一映射關(guān)系切合瓦宜。

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

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

從Looper的成員變量我們可以知道Looper包含了以下東西:

  • sMainLooper:應(yīng)用主線程的Looper蔚万,創(chuàng)建其他線程的Looper時為null
  • mQueue:Looper關(guān)聯(lián)的MessageQueue
  • mThread:Looper關(guān)聯(lián)的線程
  • sThreadLocal:線程局部變量的Key

從這可以知道,一個線程對應(yīng)一個Looper临庇,一個Looper對應(yīng)一個MessageQueue

----------------分割線反璃,接下來回到消息的投遞結(jié)束的地方----------------

得到Message后昵慌,就會通過Handler的sendMessageAtTime調(diào)用MessageQueue的enqueueMessage將Message投遞到MessageQueue中,在往下學習之前必須先了解Handler的創(chuàng)建淮蜈,因為后面的知識和它有關(guān)聯(lián)斋攀。

現(xiàn)在我們知道Message將要投遞到哪里的MessageQueue里了,那么投遞過去之后礁芦,消息是怎么被處理的呢蜻韭?這代碼很長,而且就是個進入隊列的過程柿扣,我就不貼了肖方,做了以下事情:

  1. 合法性檢查
  2. 標記Message正在使用
  3. 入列
  4. 喚醒native的MessageQueue

在這里有個有意思的概念必須提一下,就是Barrier Message未状,它表示的是一種柵欄的概念俯画,將它加入MessageQueue可以攔住所有執(zhí)行時間在它之后的同步Message,異步Message則不受影響司草,遍歷到就會處理艰垂,這種狀況會持續(xù)到把Barrier Message移除。

提示:圖里綠色代表Message可以被取出執(zhí)行埋虹,紅色表示無法被取出執(zhí)行

它和Message的根本差別是猜憎,他沒有target,即:沒有處理該Message的Handler搔课,但我們自己將Message的Handler設(shè)為null是沒法加入MessageQueue的胰柑,必須調(diào)用postSyncBarrier方法:

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    ……
}

private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;

        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

消息的分發(fā)

前面已經(jīng)知道Message投遞后就會到達MessageQueue,接下來就看消息是怎么被遍歷處理的爬泥。首先要知道的一點是柬讨,Looper在調(diào)用prepare創(chuàng)建后,是必須調(diào)loop()方法的袍啡,很多人會問踩官,我平常用的時候沒用loop()方法也沒問題啊。那是因為你是在主線程用的境输,主線程在創(chuàng)建Looper的時候已經(jīng)調(diào)用過loop()方法了蔗牡。

我們創(chuàng)建了其他線程的Looper后,調(diào)loop()方法會做以下事情:

  1. 循環(huán)獲取MessageQueue中的Message
  2. 將Message通過Handler的dispatchMessage方法分發(fā)到對應(yīng)的Handler中
  3. 將Message的信息清空嗅剖,回收到Message Pool中等待下一次使用
public static void loop() {
    ……

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

        ……
        
        try {
            msg.target.dispatchMessage(msg);
        } finally {
            ……
        }

        ……

        msg.recycleUnchecked();
    }
}

在Handler的dispatchMessage中蛋逾,對Message的處理其實是有優(yōu)先順序這個說法的:

  1. 如果Message設(shè)置了callback,則將Message交給Message的callback處理
  2. 如果Handler設(shè)置了callback窗悯,則將Message先交給Handler的callback處理
  3. 否則的話区匣,將Message交給Handler的handleMessage處理
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

對于MessageQueue,它實際表示了Java層和Native層的MessageQueue,Java層的MessageQueue就是mMessages表示的循環(huán)隊列亏钩,Native層的MessageQueue就是mPtr莲绰。它的next()方法里做的事情如下:

  1. 調(diào)用nativePollOnce讓native層的MessageQueue先處理Native層的Message,再處理Java層的Message姑丑,這個過程可能阻塞
  2. 如果在按時序遍歷MessageQueue的過程中發(fā)現(xiàn)了Barrier Message蛤签,即handler為空的Message,則跳過它后面的所有同步Message栅哀,只處理異步Message
  3. 如果消息是延時消息震肮,計算當前時間和目標時間的差值,休眠這個時間差后再去取這個Message
  4. 如果消息不是延時消息留拾,在Message Pool里標記該Message正在使用戳晌,并返回它

Java層Android消息機制的整個過程可以用下圖概括:

有鉆研過Java層代碼的朋友肯定知道,Handler里面還有個用于跨進程Message通信的MessengerImpl痴柔,這個東西我就不在這里說了沦偎,因為它就是個簡單的跨進程通信,和整個Handler咳蔚、Looper豪嚎、MessageQueue其實關(guān)系不大。

Android Native層消息機制

Android消息機制在Native層其實和Java層很相似谈火,保留了Handler侈询、Looper、MessageQueue的結(jié)構(gòu)糯耍。但是Native層Message扔字、Handler、MessageQueue的概念被弱化得很厲害谍肤,基本上只是個“空殼”啦租,核心邏輯都在Looper里邊了哗伯。

其他區(qū)別都不大了荒揣,只是在實現(xiàn)上有一點不一樣,具體的差別就在源碼中找答案吧焊刹。整體結(jié)構(gòu)圖如下:

消息的產(chǎn)生

在Native層中系任,消息由MessageEnvelope和封裝fd(Java層Handler可以添加fd的監(jiān)聽、Native當然也可以)相關(guān)信息后得到的epoll_event組成虐块。

fd

對于要被監(jiān)聽的fd的消息俩滥,Looper做了以下事情:

  1. 合法性檢查
  2. 將相關(guān)信息封裝到Request中,并初始化為epoll_event
  3. 將該fd以及要監(jiān)聽的epoll_event事件(步驟2轉(zhuǎn)換Request得到)注冊到當前Looper的epollFd中
  4. 如果出錯贺奠,進行出錯處理
  5. 更新mRequests
int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {
    ……

    { // acquire lock
        AutoMutex _l(mLock);

        ……

        struct epoll_event eventItem;
        request.initEventItem(&eventItem);

        ssize_t requestIndex = mRequests.indexOfKey(fd);
        if (requestIndex < 0) {
            int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, & eventItem);
            ……
            mRequests.add(fd, request);
        } else {
            int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_MOD, fd, & eventItem);
            ……
            mRequests.replaceValueAt(requestIndex, request);
        }
    } // release lock
    return 1;
}

MessageEnvelope

MessageEnvelope相對于fd就簡單多了霜旧,在調(diào)用Native層Looper的sendMessage相關(guān)函數(shù)時會將uptime、MessageHandler儡率、Native層Message封裝到MessageEnvelope中挂据,然后插入mMessageEnvelopes中以清。

void Looper::sendMessageAtTime(nsecs_t uptime, const sp<MessageHandler>& handler, const Message& message) {
    ……
    size_t i = 0;
    { // acquire lock
        AutoMutex _l(mLock);

        size_t messageCount = mMessageEnvelopes.size();
        while (i < messageCount && uptime >= mMessageEnvelopes.itemAt(i).uptime) {
            i += 1;
        }

        MessageEnvelope messageEnvelope(uptime, handler, message);
        mMessageEnvelopes.insertAt(messageEnvelope, i, 1);

        ……
    }
    ……
}

消息的投遞和處理

前面已經(jīng)提到了,Java層的MessageQueue處理消息時崎逃,會先調(diào)用Native層MessageQueue的nativePollOnce()掷倔,它實際調(diào)用的是native層MessageQueue的pollOnce(),而native的pollOnce調(diào)用的是Native層的Looper的pollOnce:

static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
        jlong ptr, jint timeoutMillis) {
    ……
    nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}

void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
    ……
    mLooper->pollOnce(timeoutMillis);
    ……
}

在看Native層Looper的pollOnce方法之前个绍,先看看Native層的Looper和Java層的Looper會不會有一些不一樣吧勒葱。

Looper Native

和Java層Looper的使用一樣,Native層Looper也需要prepare巴柿,也是一個通過線程局部變量存儲的對象凛虽,一個線程只有一個。那么在Native層是怎么實現(xiàn)線程局部變量的呢篮洁?

Linux TSD(Thread-specific Data)池

Native層線程局部變量的思想和Java層很類似涩维,Native層會維護一個全局的pthread_keys數(shù)組,用于存放線程局部變量的鍵袁波。其中seq用于標記是否"in_use"瓦阐,destr則是一個函數(shù)指針,可用作析構(gòu)函數(shù)篷牌,在線程退出時釋放該鍵對應(yīng)于線程中的線程局部變量睡蟋。

static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] ={{0,NULL}};

int pthread_key_create(pthread_key_t *key, void (*destr_function) (void*));

struct pthread_key_struct
{
  /* Sequence numbers.  Even numbers indicated vacant entries.  Note
     that zero is even.  We use uintptr_t to not require padding on
     32- and 64-bit machines.  On 64-bit machines it helps to avoid
     wrapping, too.  */
  uintptr_t seq;

  /* Destructor for the data.  */
  void (*destr) (void *);
};

pthread在創(chuàng)建線程時會維護一個指針數(shù)組,數(shù)組元素指向線程局部變量的數(shù)據(jù)塊枷颊。整體解構(gòu)如下圖:

創(chuàng)建Looper

創(chuàng)建Looper時戳杀,會做以下事情:

  1. 通過eventfd創(chuàng)建mWakeEventFd用于線程間通信去喚醒Looper的,當需要喚醒Looper時夭苗,就往里面寫1
  2. 創(chuàng)建用于監(jiān)聽epoll_event的mEpollFd信卡,并初始化mEpollFd要監(jiān)聽的epoll_event類型
  3. 通過epoll_ctl將mWakeEventFd注冊到mEpollFd中,當mWakeEventFd有事件可讀則喚醒Looper
  4. 如果mRequests不為空的話题造,說明前面注冊了有要監(jiān)聽的fd傍菇,則遍歷mRequests中的Request,將它初始化為epoll_event并通過epoll_ctl注冊到mEpollFd中界赔,當有可讀事件同樣喚醒Looper
Looper::Looper(bool allowNonCallbacks) :
        mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false),
        mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false),
        mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) {
    mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    ……
    rebuildEpollLocked();
}

void Looper::rebuildEpollLocked() {
    ……

    // Allocate the new epoll instance and register the wake pipe.
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    ……

    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;
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
    ……

    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, EPOLL_CTL_ADD, request.fd, & eventItem);
        ……
    }
}

pollOnce

對于Native層Looper的pollOnce丢习,找它函數(shù)定義稍微有點隱秘,它在Looper.h中聲明淮悼,inline到pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData)函數(shù)里了咐低,它做了以下事情:

  1. 優(yōu)先處理mResponses里的Response,即來自fd的事件
  2. 如果沒有需處理的Response袜腥,再調(diào)用pollInner
inline int pollOnce(int timeoutMillis) {
    return pollOnce(timeoutMillis, NULL, NULL, NULL);
}

int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for (;;) {
        while (mResponseIndex < mResponses.size()) {
            const Response& response = mResponses.itemAt(mResponseIndex++);
            int ident = response.request.ident;
            if (ident >= 0) {
                ……
                return ident;
            }
        }

        if (result != 0) {
#if DEBUG_POLL_AND_WAKE
            ALOGD("%p ~ pollOnce - returning result %d", this, result);
#endif
            ……
            return result;
        }

        result = pollInner(timeoutMillis);
    }
}

pollInner這個函數(shù)比較長见擦,它做了以下事情:

  1. 基于下一個Message調(diào)整獲取Message的時間間隔timeoutMillis
  2. 清空mResponses
  3. 獲取epoll事件,即將要處理的Message
  4. 更新mPolling,防止進入idle
  5. 執(zhí)行合法性檢查
  6. 如果epoll_event的fd為mWakeFd鲤屡,說明是Looper的喚醒事件儡湾,則喚醒Looper
  7. 否則先將epoll_event封裝為Request,更新epoll_event的事件類型执俩,再封裝為Response裝入mResponses
  8. 循環(huán)取出mMessageEnvelopes隊頭的MessageEnvelope徐钠,并將MessageEnvelope中的Message交給對應(yīng)的Native層的Handler處理
  9. 循環(huán)調(diào)用mResponses中所有Response的callback

至此對Android消息機制的學習就結(jié)束啦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末役首,一起剝皮案震驚了整個濱河市尝丐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌衡奥,老刑警劉巖爹袁,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異矮固,居然都是意外死亡失息,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門档址,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盹兢,“玉大人,你說我怎么就攤上這事守伸∫锩耄” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵尼摹,是天一觀的道長见芹。 經(jīng)常有香客問我,道長蠢涝,這世上最難降的妖魔是什么玄呛? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮和二,結(jié)果婚禮上徘铝,老公的妹妹穿的比我還像新娘。我一直安慰自己儿咱,他們只是感情好庭砍,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布场晶。 她就那樣靜靜地躺著混埠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诗轻。 梳的紋絲不亂的頭發(fā)上钳宪,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音,去河邊找鬼吏颖。 笑死搔体,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的半醉。 我是一名探鬼主播疚俱,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼缩多!你這毒婦竟也來了呆奕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤衬吆,失蹤者是張志新(化名)和其女友劉穎梁钾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逊抡,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡姆泻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了冒嫡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拇勃。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖孝凌,靈堂內(nèi)的尸體忽然破棺而出潜秋,到底是詐尸還是另有隱情,我是刑警寧澤胎许,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布峻呛,位于F島的核電站,受9級特大地震影響辜窑,放射性物質(zhì)發(fā)生泄漏钩述。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一穆碎、第九天 我趴在偏房一處隱蔽的房頂上張望牙勘。 院中可真熱鬧,春花似錦所禀、人聲如沸方面。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恭金。三九已至,卻和暖如春褂策,著一層夾襖步出監(jiān)牢的瞬間横腿,已是汗流浹背颓屑。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耿焊,地道東北人揪惦。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像罗侯,于是被迫代替她去往敵國和親器腋。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容