Handler原理的整理及相關(guān)提問點

前言

有關(guān)handler的使用網(wǎng)上的教程有很多舔亭,并不是很難,在我的前幾篇文章中也有簡單的介紹過handler使用的整理蟀俊,但是有關(guān)其實現(xiàn)的方式確實面試中非常喜歡問到的。不過以一次我被問到了handler的延遲的實現(xiàn)原理订雾,當時我很郁悶并沒有深入的去研究其delay的原理肢预,然而對其handler的流程的學習也是跳過了延遲這部分,說明還是學的不精洼哎,后來再我深入學習這部分的時候烫映,發(fā)現(xiàn)這里對handler的學習是很有幫助的沼本。

MessageQueue

首先大家都知道,這個是消息隊列锭沟,是我們管理發(fā)布消息的地方抽兆,然而我看到網(wǎng)上有人說這個是先進先出的,其實并不是這樣族淮,進確實是按照發(fā)布的順序進入的辫红,而出并不是。下面我詳細介紹這個類祝辣。
Message的功能贴妻? MessageQueue主要包含倆個操作,插入蝙斜、讀取名惩,而讀取會伴隨著刪除操作,對應(yīng)的方法就是enqueueMessage和next孕荠。然而盡管我們叫它消息隊列娩鹉,而其內(nèi)部的實現(xiàn)并不是隊列而是單鏈表的數(shù)據(jù)結(jié)構(gòu)。下面我們分析一下它的倆個主要方法的源碼稚伍。

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) {
            //插入消息到鏈表的頭部:MessageQueue實例的頭結(jié)點Message進行觸發(fā)時間先后的比較底循,  
            //如果觸發(fā)時間比現(xiàn)有的頭結(jié)點Message短,或者現(xiàn)有的鏈表頭部Message觸發(fā)時間為0槐瑞,  
            //亦或是當前MessageQueue中消息鏈表為空熙涤,則這個新的Message作為整個  
            //MessageQueue的頭結(jié)點,如果阻塞著困檩,則立即喚醒線程處理  
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                //插入消息到鏈表的中間:如果插入消息到鏈表頭部的條件不具備祠挫,則依次                  
                //循環(huán)消息鏈表比較觸發(fā)時間的長短,然后將消息插入到消息鏈表的合適位置悼沿。接著  
                //如果需要喚醒線程處理則調(diào)用C++中的nativeWake()函數(shù).  
                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;
    }
emmmmmm.jpg

延遲進入隊列哎壳?這里為了方便理解董栽,我將官方的注釋改為中文理解的注釋。我們知道,我們所有的post陕悬,postDelay,sendMessage喳坠,sendMessageDelay都會最終調(diào)用MessageQueue的enqueueMessage方法杈曲,也就是插入隊列,可以看到我們在插入隊列時候并沒有任何的阻塞操作和延遲操作非驮,其中主要的內(nèi)容只有一個if else和一個循環(huán)交汤,和一個喚醒操作,也就是說網(wǎng)上說的延遲進入消息隊列的說法是錯誤的劫笙。而這個方法的返回值是其進入到了隊列中就會返回true芙扎。

先進先出星岗?其message在發(fā)布后,會馬上進入到消息隊列中戒洼,并不會延遲進入俏橘。其中if和else可以參照注釋,if中是有消息進入并不伴隨著延遲圈浇,就是要立即looper的消息寥掐,我們的enqueueMessage會使用if將它插入到鏈表的頭部,而else呢汉额,根據(jù)代碼和注釋它是根據(jù)其delay的時間曹仗,在使用遍歷將其插入到鏈表的合適的位置,所以說消息隊列是不存在先進先出的蠕搜。

喚醒什么怎茫?而needWake是什么呢,顧名思義妓灌,他是需要喚醒轨蛤,也就是這個標識量為true的時候,隊列處于阻塞狀態(tài)虫埂,需要喚醒祥山,在if的neekWake=mBlocked中就是將阻塞的標識給了需要喚醒這個標識。

所以掉伏,既然我們不是先進先出了缝呕,那么我先postDelay(,3000)斧散,然后馬上post的話供常,那么后面的消息會阻塞3000ms嗎?當然不會鸡捐,有些人會誤認為它是隊列栈暇,先進先出,然而在后面的消息到達后箍镜,我們的隊列根據(jù)他的喚醒標識源祈,去喚醒隊列的阻塞,將這個消息插入到消息隊列的頭部色迂,如果沒有延遲則立刻調(diào)用looper()香缺。


讓我好好爆爆你.jpg

下面我們看下next方法的實現(xiàn)。

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

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

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

怎么又有循環(huán)脚草?next的代碼主要功能是從隊列中取出消息赫悄,然后再刪除對應(yīng)的消息,我們可以看到這里面有個循環(huán)馏慨,就是其實現(xiàn)取消息埂淮,整個循環(huán)是實現(xiàn)了取,而looper中的循環(huán)是讓MessageQueue不停地取写隶,也就是說這個next中如果取到了消息倔撞,并刪除,然后返回message慕趴,這個循環(huán)就停止痪蝇,也就是取消息成功!而我們知道looper中還有一個循環(huán)冕房,便實現(xiàn)了不停的取躏啰,這個取成功了繼續(xù)去取,一個輪詢耙册,倆個循環(huán)不能混淆给僵。

這里有阻塞?next的調(diào)用就是在looper中详拙。我們可以很親切的官方注釋說這個方法會阻塞帝际,我們在next中看到了這樣一個方法,nativePollOnce(ptr, nextPollTimeoutMillis);饶辙,這個方法就是實現(xiàn)了阻塞蹲诀,具體的實現(xiàn)是在C層,介紹一下這個方法弃揽。
1.如果nextPollTimeoutMillis=-1脯爪,一直阻塞不會超時。
2.如果nextPollTimeoutMillis=0矿微,不會阻塞痕慢,立即返回。
3.如果nextPollTimeoutMillis>0冷冗,最長阻塞nextPollTimeoutMillis毫秒(超時)守屉,如果期間有程序喚醒會立即返回。


666666.jpg

這個真的是厲害蒿辙,本身像wait一樣但是還是sleep的功能拇泛,而且如果有新消息到來時,還可以在enqueueMessage中通過nativePollOnce進行喚醒思灌,那么看到這里俺叭,我們應(yīng)該知道了這個postDelay如何實現(xiàn)的延遲了吧。

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

阻塞邏輯在nativePollOnce里面泰偿?是的熄守,時間未到,next中則去改變這個nextPollTimeoutMillis,然后根據(jù)這個標識裕照,在nativePollOnce方法內(nèi)部去通過邏輯實現(xiàn)是否要去阻塞攒发,阻塞多久。

sleep和postdelay的區(qū)別晋南?那么網(wǎng)上有一種區(qū)分sleep和postDelay的說法看來是錯誤的惠猿,他們說sleep實現(xiàn)了阻塞而delay沒有,實際上并不是负间,sleep是線程睡眠偶妖,而delay也是實現(xiàn)了阻塞,只不過為什么delay后面的代碼還是立即執(zhí)行政溃,原因就是之前提到的enqueueMessage中趾访,我們新的消息插入到了隊列頭,我們知道整個android的ui線程也是一個消息董虱,我們在postdelay后立刻setText扼鞋,而setText的這個消息邊會喚醒這個隊列,從而我們以為它只是延遲而沒有阻塞空扎。

主線程不是卡主了藏鹊?那么這里有人提問了,比如我在一個線程中去阻塞转锈,比如主線程盘寡,他不就卡主了嘛?當然不會撮慨,我們在主線程new Handler.postDelay(new MyRunable,3000);后會有大量的操作進入我們主線程的這個looper當中竿痰,當然我們的主線程有且只有這一個loop(可以不在子線程創(chuàng)建looper就不會有了...),我們的手的各種操作都會觸發(fā)這個looper砌溺,我們的屏幕的16ms的刷新也會觸發(fā)影涉,所以我們的這個looper這個循環(huán)不會停止,主線程停止了不就結(jié)束了嘛规伐,在我們的ActivityThread中這個循環(huán)會不停的下去蟹倾,去發(fā)消息,去觸發(fā)生命周期猖闪,當然除非我們在生命周期去進行大量的耗時操作鲜棠,這個ui線程才會真正意義上的阻塞,而5秒沒有喚醒就是觸發(fā)了ANR培慌。然而我們的主線程的很多時候都阻塞的豁陆,不然它的開銷是太大的,需要的時候喚醒就好了吵护。

子線程呢盒音?這里有人會問如果我在一個子線程表鳍,讓它的looper也在這個子線程,它的enqueueMessage也在這個子線程祥诽,它阻塞了怎么辦譬圣?我們可以試一下。
當然我們直接在子線程中去new Handler().postDelay是無法實現(xiàn)的原押,因為子線程的looper還來不及去創(chuàng)建便去通過looper去操作消息會拋出異常胁镐。
這里我們可以使用handlerThread偎血,重寫里面的run方法诸衔。

@Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
            Log.i("zhou","!!!"+getThreadId());
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    Log.i("zhou","3000");
                    Log.i("zhou","~~~"+getThreadId());
                }
            },3000);
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
有病.gif

作者是不是有病颇玷?好嘛笨农,真的是有病,不過我們試一下帖渠。他是可以打印出來內(nèi)容的谒亦,但是不是阻塞了嘛?enqueue不像是主線程不停有新消息進入去喚醒空郊,我們別忘了之前說的份招。如果nextPollTimeoutMillis>0,最長阻塞nextPollTimeoutMillis毫秒(超時)狞甚,如果期間有程序喚醒會立即返回锁摔。也就是說喚醒的方式有倆種,一種是在插入鏈表的時候哼审,一種是延遲時間到達的時候谐腰。所以理解handler的阻塞對理解handler是很有幫助的。

Looper

looper是什么涩盾?網(wǎng)上說looper是一個泵十气,這么說挺有意思的,Looper在消息機制中扮演著消息循環(huán)的角色春霍,具體說它就是不停地從MessageQueue中查看是否有新消息砸西,通過輪詢next方法去實現(xiàn)不停地查看,如果有就通過next返回址儒,沒有就通過標志量-1而阻塞芹枷。這里我們知道了,looper在哪個線程离福,里面的dispatchMessage就是哪個線程杖狼,其回調(diào)就是哪個線程,也是通過這種方式實現(xiàn)的線程通訊妖爷。
looper怎么退出蝶涩?MessageQueue唯一跳出循環(huán)的方式是MessageQueue的next方法返回null理朋,這樣looper會通過quit或者quitSafely方法來退出。

public final class Looper {
    /*
     * API Implementation Note:
     *
     * This class contains the code required to set up and manage an event loop
     * based on MessageQueue.  APIs that affect the state of the queue should be
     * defined on MessageQueue or Handler rather than on Looper itself.  For example,
     * idle handlers and sync barriers are defined on the queue whereas preparing the
     * thread, looping, and quitting are defined on the looper.
     */

    private static final String TAG = "Looper";

    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static Looper sMainLooper;  // guarded by Looper.class

    final MessageQueue mQueue;
    final Thread mThread;

    private Printer mLogging;
    private long mTraceTag;

    /* If set, the looper will show a warning log if a message dispatch takes longer than time. */
    private long mSlowDispatchThresholdMs;

     /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        prepare(true);
    }

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

    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

    /**
     * Returns the application's main looper, which lives in the main thread of the application.
     */
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

    /**
     * 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();

        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 slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

            final long traceTag = me.mTraceTag;
            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }
            final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            final long end;
            try {
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (slowDispatchThresholdMs > 0) {
                final long time = end - start;
                if (time > slowDispatchThresholdMs) {
                    Slog.w(TAG, "Dispatch took " + time + "ms on "
                            + Thread.currentThread().getName() + ", h=" +
                            msg.target + " cb=" + msg.callback + " msg=" + msg.what);
                }
            }

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

    /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

    /**
     * Return the {@link MessageQueue} object associated with the current
     * thread.  This must be called from a thread running a Looper, or a
     * NullPointerException will be thrown.
     */
    public static @NonNull MessageQueue myQueue() {
        return myLooper().mQueue;
    }

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

    /**
     * Returns true if the current thread is this looper's thread.
     */
    public boolean isCurrentThread() {
        return Thread.currentThread() == mThread;
    }

    /**
     * Control logging of messages as they are processed by this Looper.  If
     * enabled, a log message will be written to <var>printer</var>
     * at the beginning and ending of each message dispatch, identifying the
     * target Handler and message contents.
     *
     * @param printer A Printer object that will receive log messages, or
     * null to disable message logging.
     */
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

    /** {@hide} */
    public void setTraceTag(long traceTag) {
        mTraceTag = traceTag;
    }

    /** {@hide} */
    public void setSlowDispatchThresholdMs(long slowDispatchThresholdMs) {
        mSlowDispatchThresholdMs = slowDispatchThresholdMs;
    }

    /**
     * Quits the looper.
     * <p>
     * Causes the {@link #loop} method to terminate without processing any
     * more messages in the message queue.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p><p class="note">
     * Using this method may be unsafe because some messages may not be delivered
     * before the looper terminates.  Consider using {@link #quitSafely} instead to ensure
     * that all pending work is completed in an orderly manner.
     * </p>
     *
     * @see #quitSafely
     */
    public void quit() {
        mQueue.quit(false);
    }

    /**
     * Quits the looper safely.
     * <p>
     * Causes the {@link #loop} method to terminate as soon as all remaining messages
     * in the message queue that are already due to be delivered have been handled.
     * However pending delayed messages with due times in the future will not be
     * delivered before the loop terminates.
     * </p><p>
     * Any attempt to post messages to the queue after the looper is asked to quit will fail.
     * For example, the {@link Handler#sendMessage(Message)} method will return false.
     * </p>
     */
    public void quitSafely() {
        mQueue.quit(true);
    }

    /**
     * Gets the Thread associated with this Looper.
     *
     * @return The looper's thread.
     */
    public @NonNull Thread getThread() {
        return mThread;
    }

    /**
     * Gets this looper's message queue.
     *
     * @return The looper's message queue.
     */
    public @NonNull MessageQueue getQueue() {
        return mQueue;
    }

    /**
     * Dumps the state of the looper for debugging purposes.
     *
     * @param pw A printer to receive the contents of the dump.
     * @param prefix A prefix to prepend to each line which is printed.
     */
    public void dump(@NonNull Printer pw, @NonNull String prefix) {
        pw.println(prefix + toString());
        mQueue.dump(pw, prefix + "  ", null);
    }

    /**
     * Dumps the state of the looper for debugging purposes.
     *
     * @param pw A printer to receive the contents of the dump.
     * @param prefix A prefix to prepend to each line which is printed.
     * @param handler Only dump messages for this Handler.
     * @hide
     */
    public void dump(@NonNull Printer pw, @NonNull String prefix, Handler handler) {
        pw.println(prefix + toString());
        mQueue.dump(pw, prefix + "  ", handler);
    }

    /** @hide */
    public void writeToProto(ProtoOutputStream proto, long fieldId) {
        final long looperToken = proto.start(fieldId);
        proto.write(LooperProto.THREAD_NAME, mThread.getName());
        proto.write(LooperProto.THREAD_ID, mThread.getId());
        proto.write(LooperProto.IDENTITY_HASH_CODE, System.identityHashCode(this));
        mQueue.writeToProto(proto, LooperProto.QUEUE);
        proto.end(looperToken);
    }

    @Override
    public String toString() {
        return "Looper (" + mThread.getName() + ", tid " + mThread.getId()
                + ") {" + Integer.toHexString(System.identityHashCode(this)) + "}";
    }
}

它的構(gòu)造方法中會創(chuàng)建一個MessageQueue绿聘,然后保存線程的對象嗽上。
子線程記得要創(chuàng)建looper looper.prepare就可以創(chuàng)建 looper可以開啟循環(huán),也可以通過HandlerThread它在run里面已經(jīng)給我們創(chuàng)建了熄攘,并且還伴有wait方法在控制異常兽愤。
我們可以看到looper里面就是一個循環(huán),去next()挪圾,然后去msg.target.dispatchMessage浅萧,在去回調(diào)handleMessage()。

轉(zhuǎn)載請注明出處:http://www.reibang.com/p/bed3c5192a55

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哲思,一起剝皮案震驚了整個濱河市洼畅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棚赔,老刑警劉巖帝簇,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異靠益,居然都是意外死亡丧肴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門胧后,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芋浮,“玉大人,你說我怎么就攤上這事绩卤⊥狙” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵濒憋,是天一觀的道長何暇。 經(jīng)常有香客問我,道長凛驮,這世上最難降的妖魔是什么裆站? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮黔夭,結(jié)果婚禮上宏胯,老公的妹妹穿的比我還像新娘。我一直安慰自己本姥,他們只是感情好肩袍,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婚惫,像睡著了一般氛赐。 火紅的嫁衣襯著肌膚如雪魂爪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天艰管,我揣著相機與錄音滓侍,去河邊找鬼。 笑死牲芋,一個胖子當著我的面吹牛撩笆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缸浦,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼夕冲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了餐济?” 一聲冷哼從身側(cè)響起耘擂,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎絮姆,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秩霍,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡篙悯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了铃绒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸽照。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖颠悬,靈堂內(nèi)的尸體忽然破棺而出矮燎,到底是詐尸還是另有隱情,我是刑警寧澤赔癌,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布诞外,位于F島的核電站,受9級特大地震影響灾票,放射性物質(zhì)發(fā)生泄漏峡谊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一刊苍、第九天 我趴在偏房一處隱蔽的房頂上張望既们。 院中可真熱鬧,春花似錦正什、人聲如沸啥纸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽斯棒。三九已至馒索,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間名船,已是汗流浹背绰上。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留渠驼,地道東北人蜈块。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像迷扇,于是被迫代替她去往敵國和親百揭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345