深入理解Handler機制

Handler并不是專門用于更新UI稚配,它只是常被開發(fā)者用來更新UI。

Android的消息機制主要指Handler的運行機制柏副,底層需要MessageQueue和Looper的支撐勾邦。MessageQueue是采用單鏈表的數(shù)據(jù)結(jié)構(gòu)來存儲消息列表的,Looper為消息循環(huán)割择。由于MessageQueue是一個消息的存儲單元眷篇,不能處理信息,Looper就填補了這一功能荔泳,Looper會以無限循環(huán)的方式去查找是否有新消息蕉饼,有就處理,沒有就等待玛歌。Looper還有一個ThreadLocal昧港,可以在每個線程中存儲數(shù)據(jù)。通過ThreadLocal可以獲取每個線程的Looper沾鳄,一般線程默認是沒有Looper慨飘,除了UI線程,所以就必須創(chuàng)建Looper译荞。

一瓤的、Android消息機制概述

Handler的主要功能是將一個任務(wù)切換到某個指定的線程中去操作,這是因為Android規(guī)定UI操作只能在主線程吞歼,不然就會拋出異常圈膏,原因是因為ViewRootImpl對UI操作做了驗證,通過ViewRootImpl的checkThread方法來完成篙骡。

系統(tǒng)提供Handler的主要原因是為了解決子線程中無法訪問到UI的矛盾稽坤。

這是因為Android的UI控件不是線程安全的,如果多線程訪問會導(dǎo)致UI處于不可預(yù)期狀態(tài)糯俗,如果加上鎖機制尿褪,會有兩個問題:

  • 加上鎖會導(dǎo)致讓UI訪問的邏輯變得復(fù)雜
  • 降低UI的訪問效率

一般創(chuàng)建Handler的時候需要有Looper,不然會出現(xiàn)異常得湘。

image

Handler創(chuàng)建以后杖玲,會同內(nèi)部的Looper以及MessageQueue一起協(xié)同工作,然后Handler的post方法將一個Runanble投遞到Handler內(nèi)部的Looper中去處理淘正,其實post也是通過send方法來完成的摆马,當調(diào)用send方法時臼闻,會調(diào)用MessageQueue的enqueueMessage方法將消息放到消息隊列中,然后Looper發(fā)現(xiàn)有新的消息來時囤采,則會處理這個消息述呐,最后消息中的Runnable或者Handler的handleMessage方法會被調(diào)用。注意Looper是運行在創(chuàng)建Handler所在的線程中的蕉毯,這樣一來Handler中的業(yè)務(wù)邏輯就被切換到創(chuàng)建Handler所在的線程中去執(zhí)行乓搬。


二、Android的消息機制分析

1.ThreadLocal的工作原理

ThreadLocalThreadLocal是一個線程內(nèi)部的數(shù)據(jù)存儲類代虾,通過它可以在指定的線程中存儲數(shù)據(jù)缤谎,數(shù)據(jù)存儲以后,只有在指定線程中可以獲取到存儲的數(shù)據(jù)褐着,對于其它線程來說無法獲取到數(shù)據(jù)坷澡。ThreadLocal另一個使用場景是復(fù)雜邏輯下的對象傳遞,比如監(jiān)聽器的傳遞含蓉,有些時候一個線程中的任務(wù)過于復(fù)雜频敛,這可能表現(xiàn)為函數(shù)調(diào)用棧比較深以及代碼入口的多樣性,在這種情況下馅扣,我們又需要監(jiān)聽器能夠貫穿整個線程的執(zhí)行過程斟赚,這個時候可以怎么做呢?其實就可以采用ThreadLocal差油,采用ThreadLocal可以讓監(jiān)聽器作為線程內(nèi)的全局對象而存在拗军,在線程內(nèi)部只要通過get方法就可以獲取到監(jiān)聽器。

介紹了那么多ThreadLocal的知識蓄喇,可能還是有點抽象发侵,下面通過實際的例子為大家演示ThreadLocal的真正含義。首先定義一個ThreadLocal對象妆偏,這里選擇Boolean類型的刃鳄,如下所示:

private ThreadLocal<Boolean>mBooleanThreadLocal = new ThreadLocal<Boolean>();

然后分別在主線程、子線程1和子線程2中設(shè)置和訪問它的值钱骂,代碼如下所示:

mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());

new Thread("Thread#1") {
    @Override
    public void run() {
        mBooleanThreadLocal.set(false);
        Log.d(TAG, "[Thread#1]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();

new Thread("Thread#2") {
    @Override
    public void run() {
        Log.d(TAG, "[Thread#2]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
    };
}.start();

在上面的代碼中叔锐,在主線程中設(shè)置mBooleanThreadLocal的值為true,在子線程1中設(shè)置mBooleanThreadLocal的值為false见秽,在子線程2中不設(shè)置mBooleanThreadLocal的值愉烙,然后分別在3個線程中通過get方法去mBooleanThreadLocal的值,根據(jù)前面對ThreadLocal的描述解取,這個時候步责,主線程中應(yīng)該是true,子線程1中應(yīng)該是false,而子線程2中由于沒有設(shè)置值勺择,所以應(yīng)該是null,安裝并運行程序伦忠,日志如下所示:

D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true
D/TestActivity(8676):[Thread#1]mBooleanThreadLocal=false
D/TestActivity(8676):[Thread#2]mBooleanThreadLocal=null

從上面日志可以看出省核,雖然在不同線程中訪問的是同一個ThreadLocal對象,但是它們通過ThreadLocal來獲取到的值卻是不一樣的昆码,這就是ThreadLocal的奇妙之處气忠。結(jié)合這這個例子然后再看一遍前面對ThreadLocal的兩個使用場景的理論分析,大家應(yīng)該就能比較好地理解ThreadLocal的使用方法了赋咽。ThreadLocal之所以有這么奇妙的效果旧噪,是因為不同線程訪問同一個ThreadLocal的get方法,ThreadLocal內(nèi)部會從各自的線程中取出一個數(shù)組脓匿,然后再從數(shù)組中根據(jù)當前ThreadLocal的索引去查找出對應(yīng)的value值淘钟,很顯然,不同線程中的數(shù)組是不同的陪毡,這就是為什么通過ThreadLocal可以在不同的線程中維護一套數(shù)據(jù)的副本并且彼此互不干擾米母。

ThreadLocal是一個泛型類,它的定義為public class ThreadLocal<T>毡琉,只要弄清楚ThreadLocal的get和set方法就可以明白它的工作原理铁瞒。
首先看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);
}

在上面的set方法中桅滋,首先會通過values方法來獲取當前線程中的ThreadLocal數(shù)據(jù)慧耍,如果獲取呢?其實獲取的方式也是很簡單的丐谋,在Thread類的內(nèi)容有一個成員專門用于存儲線程的ThreadLocal的數(shù)據(jù)芍碧,如下所示:ThreadLocal.Values localValues,因此獲取當前線程的ThreadLocal數(shù)據(jù)就變得異常簡單了号俐。如果localValues的值為null师枣,那么就需要對其進行初始化,初始化后再將ThreadLocal的值進行存儲萧落。下面看下ThreadLocal的值到底是怎么localValues中進行存儲的践美。在localValues內(nèi)部有一個數(shù)組:private Object[] table,ThreadLocal的值就是存在在這個table數(shù)組中找岖,下面看下localValues是如何使用put方法將ThreadLocal的值存儲到table數(shù)組中的陨倡,如下所示:

void put(ThreadLocal<?> key, Object value) {
    cleanUp();

    // Keep track of first tombstone. That's where we want to go back
    // and add an entry if necessary.
    int firstTombstone = -1;

    for (int index = key.hash & mask;; index = next(index)) {
        Object k = table[index];

        if (k == key.reference) {
            // Replace existing entry.
            table[index + 1] = value;
            return;
        }

        if (k == null) {
            if (firstTombstone == -1) {
                // Fill in null slot.
                table[index] = key.reference;
                table[index + 1] = value;
                size++;
                return;
            }

            // Go back and replace first tombstone.
            table[firstTombstone] = key.reference;
            table[firstTombstone + 1] = value;
            tombstones--;
            size++;
            return;
        }

        // Remember first tombstone.
        if (firstTombstone == -1 && k == TOMBSTONE) {
            firstTombstone = index;
        }
    }
}

面的代碼實現(xiàn)數(shù)據(jù)的存儲過程,這里不去分析它的具體算法许布,但是我們可以得出一個存儲規(guī)則兴革,那就是ThreadLocal的值在table數(shù)組中的存儲位置總是為ThreadLocal的reference字段所標識的對象的下一個位置,比如ThreadLocal的reference對象在table數(shù)組的索引為index,那么ThreadLocal的值在table數(shù)組中的索引就是index+1杂曲。最終ThreadLocal的值將會被存儲在table數(shù)組中:table[index + 1] = value庶艾。

上面分析了ThreadLocal的set方法,這里分析下它的get方法擎勘,如下所示:

public T get() {
    // Optimized for the fast path.
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values != null) {
        Object[] table = values.table;
        int index = hash & values.mask;
        if (this.reference == table[index]) {
            return (T) table[index + 1];
        }
    } else {
        values = initializeValues(currentThread);
    }

    return (T) values.getAfterMiss(this);
}

可以發(fā)現(xiàn)咱揍,ThreadLocal的get方法的邏輯也比較清晰,它同樣是取出當前線程的localValues對象棚饵,如果這個對象為null那么就返回初始值煤裙,初始值由ThreadLocal的initialValue方法來描述,默認情況下為null噪漾,當然也可以重寫這個方法硼砰,它的默認實現(xiàn)如下所示:

/**
 * Provides the initial value of this variable for the current thread.
 * The default implementation returns {@code null}.
 *
 * @return the initial value of the variable.
 */
protected T initialValue() {
    return null;
}

從ThreadLocal的set和get方法可以看出,它們所操作的對象都是當前線程的localValues對象的table數(shù)組欣硼,因此在不同線程中訪問同一個ThreadLocal的set和get方法题翰,它們對ThreadLocal所做的讀寫操作僅限于各自線程的內(nèi)部,這就是為什么ThreadLocal可以在多個線程中互不干擾地存儲和修改數(shù)據(jù)诈胜,理解ThreadLocal的實現(xiàn)方式有助于理解Looper的工作原理遍愿。

2.消息隊列的工作原理

MessageQueue主要包含兩個操作:插入和讀取,插入和讀取分為enqueueMessage和next耘斩,其中enqueueMessage是往消息隊列中插入一條消息沼填,next是從消息隊列中去出一條消息并從消息隊列中移除,MessageQueue實際上是使用單鏈表來實現(xiàn)的括授。

boolean enqueueMessage(Message msg, long when) {
        ...
        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;
    
}

它主要是實現(xiàn)單鏈表的插入操作坞笙。

Message next() {
        ....
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            }
        }
    
}

next是一個無限循環(huán)的方法,如果沒有消息荚虚,則一直阻塞薛夜,如果有消息則繼續(xù)運行。

3.Looper的工作原理

Looper在Android的消息機制中就是用來進行消息循環(huán)的版述。它會不停地循環(huán)梯澜,去MessageQueue中查看是否有新消息意狠,如果有消息就立刻處理該消息剂公,否則就一直等待趟庄。

首線來看下Looper的構(gòu)造方法:

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

通過Looper.prepare()即可為當前線程創(chuàng)建一個Looper帅掘,接著通過Looper.loop()來開啟消息循環(huán):

new Thread("Thread 2"){
    public void run(){
        Looper.prepare();
        Handler handler = new Handler();
        Looper.loop();
    }
}

Looper中除了prepare方法外,還提供了prepareMainLooper方法挺物,這個方法主要是給主線程創(chuàng)建Looper使用的忱屑,即是屬于ActivityThread的Looper篮昧,其本質(zhì)也是通過prepare來實現(xiàn)的母债,同時Looper也提供一個getMainLooper方法午磁,通過它可以在任何地方獲取ActivityThread的Looper尝抖。Looper也是可以退出的,它提供了quit和quitSafely方法來退出迅皇,二者的區(qū)別是:quit直接退出Looper昧辽,quitSafely只是設(shè)置一個退出標志,等消息隊列中的消息處理完畢登颓,就會退出Looper搅荞,這時候Handler的send方法返回false。如果在子線程中手動創(chuàng)建了Looper挺据,要記得退出Looper,不然這個線程會一直處于等待轉(zhuǎn)態(tài)脖隶,如果退出了扁耐,這個線程也會被終止。

Looper最重要的方法是loop(),只有調(diào)用才能使消息循環(huán)起來产阱,源碼實現(xiàn)如下:

    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
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            try {
                msg.target.dispatchMessage(msg);
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }

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

Looper的loop方法也很好理解婉称,loop是一個死循環(huán)的方法,唯一跳出死循環(huán)的方式是MessageQueue的next方法返回null构蹬。當Looper的quit方法被執(zhí)行的時候王暗,Looper會調(diào)用MessageQueue的quit或者quitSafely方法來通知消息隊列退出,當被標記退出庄敛,消息隊列就會返回null俗壹。loop的阻塞是因為MessageQueue的next方法阻塞造成的。如果MessageQueue的next方法返回新消息了藻烤,那就會調(diào)用msg.target.dispatchMessage(msg)來處理绷雏,其中msg.target是Handler對象,dispatchMessage雖然是通過Handler調(diào)用的怖亭,但是是在創(chuàng)建Handler時所使用的Looper中執(zhí)行的涎显,也就是在當前Looper中或者線程中執(zhí)行的,這樣就可以實現(xiàn)線程的切換兴猩。

4. Hanler的工作原理

Handler的工作主要包含消息的發(fā)送和接受過程期吓。消息的發(fā)送可以通過post或者send的一系列方法來實現(xiàn),如下所示:

    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }

    /**
     * Sends a Message containing only the what value.
     *  
     * @return Returns true if the message was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.
     */
    public final boolean sendEmptyMessage(int what)
    {
        return sendEmptyMessageDelayed(what, 0);
    }

    /**
     * Sends a Message containing only the what value, to be delivered
     * after the specified amount of time elapses.
     * @see #sendMessageDelayed(android.os.Message, long) 
     * 
     * @return Returns true if the message was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.
     */
    public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageDelayed(msg, delayMillis);
    }

    /**
     * Sends a Message containing only the what value, to be delivered 
     * at a specific time.
     * @see #sendMessageAtTime(android.os.Message, long)
     *  
     * @return Returns true if the message was successfully placed in to the 
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.
     */

    public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) {
        Message msg = Message.obtain();
        msg.what = what;
        return sendMessageAtTime(msg, uptimeMillis);
    }
    
        private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

Handler的發(fā)送消息就是向MessageQueue隊列插入一條消息倾芝,然后MessageQueue就會調(diào)用next方法返回這條消息給Looper讨勤,Looper會開始交由Handler處理,即Handler的dispatchMessage方法會被調(diào)用晨另,dispatchMessage的實現(xiàn)如下:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

Handler的消息處理過程如下:

  • 首先先檢查Message的callback是否為空悬襟,不為空就通過handleCallback來處理,Message的callback是一個Runnable對象拯刁,實際上就是Handler的post方法所傳遞的Runnable參數(shù)脊岳,handlerCallback的邏輯如下:
    private static void handleCallback(Message message) {
        message.callback.run();
    }
  • 其次檢查mCallback是否為null,不為null就調(diào)用mCallback的handleMessage方法來處理,Callback是個接口割捅,定義如下:
    public interface Callback {
        public boolean handleMessage(Message msg);
    }
  • 通過Callback可以采用如下方法創(chuàng)建Handler handler = new Handler(callable)奶躯。在日常開發(fā)中,創(chuàng)建Handler最常見的方式是派生一個Handler的子類并且重寫它的handleMessage方法亿驾,另外一種是傳入一個Callback嘹黔,并實現(xiàn)callback的handleMessage方法。

  • 最后調(diào)用Handler的handleMessage方法來處理消息莫瞬。

處理過程的流程圖:

image

Handler還有一個特殊的構(gòu)造方法儡蔓,通過一個特定的Looper來構(gòu)造Handler,實現(xiàn)一些特殊的功能疼邀。


三喂江、主線程的消息循環(huán)

Android的主線程是ActivityThread,主線程的方法入口是mian方法旁振,main方法通過Looper.prepareMainLooper()來創(chuàng)建主線程的Looper和MessageQueue获询,并通過Looper.loop()來啟動循環(huán)。主線程的消息循環(huán)開始后拐袜,需要一個Handler來進行處理和發(fā)送吉嚣,這個就是ActivityThread.H,內(nèi)部有一組消息類型。ActivityThread通過ApplicationThread和AMS進行通信蹬铺,AMS一進程間通信完成ActivityThread的請求后回調(diào)ApplicationThread的Binder方法尝哆,然后像H發(fā)送消息,H收到后會將ApplicationThread中的邏輯切換到ActivityThread中去執(zhí)行甜攀,即切換到主線程中较解。

CSDN博客:http://blog.csdn.net/aaaaa_sean_m/article/details/74079912

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赴邻,隨后出現(xiàn)的幾起案子印衔,更是在濱河造成了極大的恐慌,老刑警劉巖姥敛,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奸焙,死亡現(xiàn)場離奇詭異,居然都是意外死亡彤敛,警方通過查閱死者的電腦和手機与帆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墨榄,“玉大人玄糟,你說我怎么就攤上這事“乐龋” “怎么了阵翎?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵逢并,是天一觀的道長。 經(jīng)常有香客問我郭卫,道長砍聊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任贰军,我火速辦了婚禮玻蝌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘词疼。我一直安慰自己俯树,他們只是感情好,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布贰盗。 她就那樣靜靜地躺著许饿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪童太。 梳的紋絲不亂的頭發(fā)上米辐,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天胸完,我揣著相機與錄音书释,去河邊找鬼。 笑死赊窥,一個胖子當著我的面吹牛爆惧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锨能,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼扯再,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了址遇?” 一聲冷哼從身側(cè)響起熄阻,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倔约,沒想到半個月后秃殉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡浸剩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年钾军,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绢要。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡吏恭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出重罪,到底是詐尸還是另有隱情樱哼,我是刑警寧澤哀九,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站唇礁,受9級特大地震影響勾栗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盏筐,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一围俘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧琢融,春花似錦界牡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至纳令,卻和暖如春挽荠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背平绩。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工圈匆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捏雌。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓跃赚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親性湿。 傳聞我的和親對象是個殘疾皇子纬傲,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

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