卡頓優(yōu)化①Android UI渲染和刷新機制

丟幀和卡頓

卡頓,是字面意思上來講,就是畫面不流暢,即頁面刷新不連貫。Android系統(tǒng)默認的頁面刷新頻率是60幀,每秒刷新60次,即屏幕上的畫面16.6ms刷新一次,這個頻率是由手機設備的屏幕硬件來控制的。如果16.6ms沒有完成一次刷新,就造成了丟幀。大部分的App偶爾間歇性地出現(xiàn)幾次丟幀,不會造成明顯的卡頓,只有連續(xù)或者短時間多次出現(xiàn)丟幀,就會讓用戶感覺到明顯的卡頓現(xiàn)象。

UI渲染機制

手機屏幕由一個個的像素點組成,如1920x1080分辨率的屏幕就表示在屏幕上橫向每行有1080個像素點,縱向每列有1920個像素點。也可以想象成二維數(shù)組。數(shù)組中的每個數(shù)值,代表對應的像素點顯示的顏色。屏幕畫面每隔16.6ms刷新一次,也就是更新每個像素點要顯示的顏色。而這個16.6ms的刷新頻率,也就是60FPS,是由手機屏幕硬件來控制的。

系統(tǒng)是依據(jù)什么來確定每個像素點應該顯示的顏色呢?由此引出了Frame Buffer的概念。Frame Buffer是系統(tǒng)的幀緩沖區(qū),可以理解為屏幕顯示的抽象。
image.png
結合上面的圖示,每一個Surface都對應一個Buffer Queue,由SufaceFlinger管理。Buffer Queue內部包含兩個Graphic Buffer。頁面刷新時,繪制內容最終經過柵格化后存放在Graphic Buffer中。其中Front Buffer里存放的是頁面當前正在顯示這一幀的內容。頁面需要更新時,將先下一幀要顯示的內容存放在Offscreen Buffer,再通過Swap Buffer復制到Front Buffer中。

同時可能會有多個Surface,它們可能來自不同的應用,也可能是同一個應用里面類似SurfaceView和TextureView,它們也都有自己單獨的Surface。屏幕刷新時,SufaceFlinger從每一個Surface的Front Buffer中,拿到要顯示的內容。然后將所有Surface要顯示的內容,統(tǒng)一交給Hardware Composer,它會根據(jù)位置,Z-Order順序等信息合成最終要顯示顯示在屏幕上的內容,而這個內容就會交給系統(tǒng)的幀緩沖區(qū)Frame Buffer來顯示。也就是說,系統(tǒng)根據(jù)Frame Buffer中的內容來確定每一個像素點在每一幀要顯示的顏色。
至此,我們了解了Frame Buffer的作用,并由此引出了Surface,SurfaceFlinger,Graphic Buffer等圖形組件的概念。這里引用一篇文章中的比喻來讓大家對整個圖形繪制系統(tǒng)的整體架構有個大概的了解,然后再進一步深入了解。
如果把應用程序的頁面渲染過程當做是一次繪畫過程,那么繪畫過程中Android的各個圖形組件的作用是:

  • 畫筆:Skia或者OpenGL。我們可以用Skia畫筆來繪制2D圖形,也可以用OpenGL畫筆來繪制2D/3D圖形。前者使用CPU繪制,或者使用GPU繪制。
  • 畫布:Surface。所有的內容元素都在Surface這張畫紙上進行繪制和渲染。在Android中,Window是View的容器,每一個Window都會關聯(lián)一個Surface。而WindowManager負責管理這些Window,并把他們的數(shù)據(jù)傳遞給SurfaceFlinger。
  • 畫板:Graphic Buffer。Graphic Buffer緩沖用于應用程序的頁面繪制,在Android4.1之前使用的是雙緩沖機制。Android4.1之后,使用的是三緩沖機制。
    顯示:SurfaceFlinger。它將WindowManager提供的所有Surface,通過硬件合成器Hardware Composer合成并輸出到顯示屏。

CPU和GPU

整個UI渲染機制主要依賴三個硬件:CPU、GPU和屏幕。上面已經介紹過了。下圖則展示了在UI渲染過程中,CPU和GPU分別負責的工作。
image.png

UI組件繪制到屏幕之前,都需要經過柵格化(Rasterization)操作,而柵格化操作又是一個相對耗時的操作。相比于CPU,GPU更擅長處理圖形運算,可以加快柵格化過程。這也是通常所說的硬件加速繪制的原理,將CPU不擅長的圖形計算轉換為GPU的專有指令來處理。

Android3.0之前,或者沒有啟動硬件加速時,系統(tǒng)會使用軟件方式來渲染UI,即上圖中的軟件繪制。
image.png
整體流程是,系統(tǒng)遍歷Window中的每個View,執(zhí)行其onDraw方法,在onDraw方法中,通過傳入的Canvas對象,執(zhí)行各自的繪制邏輯。這個Canvas對象是通過Window關聯(lián)的Surface的lock函數(shù)獲取的,Canvas可以理解為Skia底層接口的封裝。View的繪制工作完成后,通過Canvas以及Skia將繪制內容柵格化到Graphic Buffer(也就是前面提到的Offscreen Buffer)中。然后經過Swap Buffer過程后,把Front Buffer里的內容交給SurfaceFlinger,最后由硬件合成器Hardware Composer合成并放入Frame Buffer,最終輸出到屏幕上。

硬件加速繪制

在軟件繪制過程中,由于CPU對圖形計算的處理不是那么高效,這個過程完全沒有利用到GPU在圖形處理方面的優(yōu)勢。所以在Android 3.0之后,Android開始支持硬件加速繪制。到Android 4.0時,系統(tǒng)會默認開啟硬件加速。
image.png

硬件加速繪制與軟件繪制的整個流程差異很大,最核心的就是硬件加速繪制是通過OpenGL ES的接口調用,由GPU完成柵格化操作,然后將繪制內容放入Graphic Buffer。此外硬件加速繪制還引入了DisplayList的概念。每個View內部都有一個DisplayList,當某個View需要重繪(比如調用invalidate方法)時,將DisplayList標記為Dirty。這樣當頁面刷新時,就只需要重繪一個View的DisplayList,而不是像軟件繪制那樣觸發(fā)整個視圖層級的遍歷。從而減少了需要重繪的View的數(shù)量,提高了繪制渲染的效率。
image.png

Peoject Butter

Google 在 2012 年的 I/O 大會上宣布了Project Butter黃油計劃,在Android4.1上,對Android Display系統(tǒng)進行了重構,引入了三個核心要素:VSYNC、Tripe Buffer和Choreographer。

VSYNC

VSYNC信號,可以理解為一種定時中斷,是一種在PC上已經很早就廣泛使用的技術。由系統(tǒng)底層控制VSYNC信號的發(fā)送頻率。由于大部分屏幕硬件的刷新頻率都是60FPS,所以VSYNC信號的發(fā)送頻率也是60FPS,即系統(tǒng)底層16.6ms發(fā)出一個VSYNC信號。每次收到VSYNC信號,CPU會立即開始計算需要繪制的數(shù)據(jù)(可以理解為執(zhí)行View的measure、layout、draw的過程),這也是應用的頁面下一幀繪制的開始,然后由GPU對數(shù)據(jù)進行柵格化并填充到Offscreen Buffer。收到VSYNC信號的同時,SurfaceFlinger開始收集每一個Surface的Front Buffer中的內容,交給Hardware Composer進行合成并輸出到Frame Buffer,最終完成屏幕刷新。也就是說系統(tǒng)每次發(fā)出VSYNC信號時,CPU開始進行下一幀繪制的準備,最終由GPU完成下一幀顯示內容的繪制處理。而SurfaceFlinger則完成的是當前這一幀內容的顯示操作。這就是雙緩沖機制的原理。
image.png
Tripe Buffer

Tripe Buffer,即三重緩沖機制,三個Graphic Buffer。如果你理解了雙緩沖機制的原理,就可以想象一下這樣一個問題。當前頁面正在顯示Front Buffer的內容,GPU正在往Offscreen Buffer填充下一幀的顯示內容,而在GPU進行這一操作時,會將Offscreen Buffer鎖定,如果CPU和GPU的繪制處理過程耗時太長,超過了一個VSYNC信號周期,就會導致本該進行Swap Buffer操作時,因為Offscreen Buffer被鎖定,無法正常進行Swap Buffer,從而導致Front Buffer里的內容也不能更新,還是保留上一幀的內容。從而出現(xiàn)丟幀現(xiàn)象。
image.png

如果再提供一個緩沖區(qū),CPU、GPU 和顯示設備都能使用各自的緩沖區(qū)工作,互不影響。簡單來說,三緩沖機制就是在雙緩沖機制基礎上增加了一個 Graphic Buffer 緩沖區(qū),這樣可以最大限度的利用VSYNC信號周期的空閑時間,帶來的壞處是多使用了一個 Graphic Buffer 所占用的內存。
image.png
ChoreoGrapher

ChoreoGrapher,主要作用是接受VSYNC信號。系統(tǒng)發(fā)出VSYNC信號的頻率是60FPS,那是不是意味著,不管App的頁面是否需要刷新,都會接收VSYNC信號,然后開始由CPU準備下一幀繪制需要的數(shù)據(jù)呢?其實不然,如果App的頁面不需要刷新,App就不會接收到VSYNC信號。只有當頁面需要刷新時,才會由ChoreoGrapher來執(zhí)行一個類似注冊監(jiān)聽VSYNC信號的操作,然后當系統(tǒng)下一次發(fā)出VSYNC信號時,ChoreoGrapher就會接收到VSYNC信號,來執(zhí)行頁面繪制的相關工作??赡苓@樣的說法有點抽象,接下來通過介紹ChoreoGrapher的相關源碼來解釋說明它的作用。
了解View的繪制機制的同學都知道,當頁面有視圖變化,需要刷新時,都會執(zhí)行到ViewRootImpl的requestLayoiut方法,很多介紹View繪制原理的文章,都以requestLayout方法作為頁面繪制過程真正的開始點。它內部又會調用scheduleTraversals方法。

//ViewRootImpl.java
@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            //mLayoutRequested用來輔助判斷是否需要執(zhí)行View的measure和layout過程
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //向主線程的Messagequeue中添加同步屏障,目的是讓頁面繪制的相關任務能盡快執(zhí)行
            //頁面繪制的相關任務是以異步消息的方式發(fā)到主線程,在添加同步屏障之后,異步消息的任務將優(yōu)先執(zhí)行
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

在scheduleTraversals方法中,我們看到了mChoreographer。此處調用了它的postCallback方法。第一個參數(shù)表示回調類型,第二個參數(shù)表示一個待執(zhí)行的任務。

//Choreographer.java
@TestApi
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
@TestApi
    public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        if (action == null) {
            throw new IllegalArgumentException("action must not be null");
        }
        if (callbackType < 0 || callbackType > CALLBACK_LAST) {
            throw new IllegalArgumentException("callbackType is invalid");
        }

        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        if (DEBUG_FRAMES) {
            Log.d(TAG, "PostCallback: type=" + callbackType
                    + ", action=" + action + ", token=" + token
                    + ", delayMillis=" + delayMillis);
        }

        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

如上述代碼所示,經過層層調用,完成實際工作的是postCallbackDelayedInternal方法,主要完成兩件事:
第一,將postCallback方法傳進來的待執(zhí)行任務,封裝成一個Callback,保存在mCallbackQueues隊列中,mCallbackQueues是CallbackQueue類型的數(shù)組。CallbackQueue是Choreographer的內部類,其實質是一個單向鏈表,其中的每一個Callback按dueTime的先后排序。

//Choreographer#CallbackQueue
@UnsupportedAppUsage
        public void addCallbackLocked(long dueTime, Object action, Object token) {
            CallbackRecord callback = obtainCallbackLocked(dueTime, action, token);
            CallbackRecord entry = mHead;
            if (entry == null) {
                mHead = callback;
                return;
            }
            if (dueTime < entry.dueTime) {
                callback.next = entry;
                mHead = callback;
                return;
            }
            while (entry.next != null) {
                if (dueTime < entry.next.dueTime) {
                    callback.next = entry.next;
                    break;
                }
                entry = entry.next;
            }
            entry.next = callback;
        }

第二件事就是向主線程中添加一個MSG_DO_SCHEDULE_VSYNC類型的異步消息,根據(jù)duetime的時序判斷是直接添加還是在指定的dutime時間添加,而添加MSG_DO_SCHEDULE_VSYNC消息的工作就是在scheduleFrameLocked方法中完成

//Choreographer.java
private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            // mFrameScheduled保證16ms內,只會申請一次垂直同步信號
            // scheduleFrameLocked可以被調用多次,但是mFrameScheduled保證下一個vsync到來之前,不會有新的請求發(fā)出
            // 多余的scheduleFrameLocked調用被無效化
            mFrameScheduled = true;
            if (USE_VSYNC) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame on vsync.");
                }

                // If running on the Looper thread, then schedule the vsync immediately,
                // otherwise post a message to schedule the vsync from the UI thread
                // as soon as possible.
                if (isRunningOnLooperThreadLocked()) {
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
                }
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }

首先,需要注意的是mFrameScheduled,它是當前Choreographer是否正在監(jiān)聽VSYNC的標識,同時也能防止重復監(jiān)聽,Choreographer每次接收到VSYNC信號時,將mFrameScheduled置為false,當需要監(jiān)聽VSYNC信號時,再將mFrameScheduled置為true。這就意味著Choreographer每次接收到VSYNC信號,處理完后續(xù)邏輯,之后監(jiān)聽下一個VSYNC信號時,需要重新注冊。
根據(jù)代碼中的注釋,方法開頭的USE_VSYNC用來區(qū)分是否啟用了vysnc機制,默認為true。我們主要關注的也是USE_VSYNC為true的情況。同樣根據(jù)代碼中的注釋,如果當前線程是主線程就直接執(zhí)行scheduleVsyncLocked方法,否則就通過異步消息的方式,讓主線程執(zhí)行scheduleVsyncLocked方法

ChoreoGrapher.java
@UnsupportedAppUsage
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }

mDisplayEventReceiver是抽象類DisplayEventReceiver的對象。它是來定義SYNC信號的接收器。它的主要功能有兩個:“注冊”VSYNC信號的監(jiān)聽和接收VSYNC信號。它的scheduleVsync方法內部會調用nativeScheduleVsync方法,這個native方法是最終實現(xiàn)VSYNC信號監(jiān)聽的"注冊"。這里不再對nativeScheduleVsync進一步分析,所以這里說的注冊,并不一定和Android的BroadcasrReceiver機制的注冊一個概念。有興趣的同學可以繼續(xù)深入到native方法中進一步了解。

@UnsupportedAppUsage
    public void scheduleVsync() {
        if (mReceiverPtr == 0) {
            Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                    + "receiver has already been disposed.");
        } else {
            nativeScheduleVsync(mReceiverPtr);
        }
    }

ChoreoGrapher的內部類FrameDisplayEventReceiver,繼承自DisplayEventReceiver,重寫了onVsync方法。而從DisplayEventReceiver源碼的注釋可以得知,onVsync方法就是接收到VSYNC信號的回調方法。由于FrameDisplayEventReceiver實現(xiàn)了Runnable接口,可以將其當做是一個可執(zhí)行任務。而FrameDisplayEventReceiver的onVsync方法,就做了一件事,就是向主線程發(fā)送異步消息,在主線程中執(zhí)行它的run方法。

//Choreographer#FrameDisplayEventReceiver
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        private boolean mHavePendingVsync;
        private long mTimestampNanos;
        private int mFrame;

        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
            super(looper, vsyncSource);
        }

        // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
        // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
        // for the internal display implicitly.
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
            // Post the vsync event to the Handler.
            // The idea is to prevent incoming vsync events from completely starving
            // the message queue.  If there are no messages in the queue with timestamps
            // earlier than the frame time, then the vsync event will be processed immediately.
            // Otherwise, messages that predate the vsync event will be handled first.
            long now = System.nanoTime();
            if (timestampNanos > now) {
                Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                        + " ms in the future!  Check that graphics HAL is generating vsync "
                        + "timestamps using the correct timebase.");
                timestampNanos = now;
            }

            if (mHavePendingVsync) {
                Log.w(TAG, "Already have a pending vsync event.  There should only be "
                        + "one at a time.");
            } else {
                mHavePendingVsync = true;
            }

            mTimestampNanos = timestampNanos;
            mFrame = frame;
            Message msg = Message.obtain(mHandler, this);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        }

        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame);
        }
    }

如上述代碼所示,在主線程中通過執(zhí)行FrameDisplayEventReceiver的run方法,也就是執(zhí)行Choreographer的doFrame方法,其中需要重點關注的相關代碼如下

//Choreographer.java
@UnsupportedAppUsage
    void doFrame(long frameTimeNanos, int frame) {
            ...
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
            doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
            ...
    }

這個Choreographer.CALLBACK_TRAVERSAL很眼熟,在ViewRootImpl的schduleTraversals方法中調用Choreographer的postCallback方法時傳入的第一個參數(shù)也是Choreographer.CALLBACK_TRAVERSAL。

//Choreographer.java
void doCallbacks(int callbackType, long frameTimeNanos) {
            ...
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            ...
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                c.run(frameTimeNanos);
            }finally {
            synchronized (mLock) {
                mCallbacksRunning = false;
                do {
                    final CallbackRecord next = callbacks.next;
                    recycleCallbackLocked(callbacks);
                    callbacks = next;
                } while (callbacks != null);
            }
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
            ....
}

在doCallbacks方法中,根據(jù)callbackType也就是Choreographer.CALLBACK_TRAVERSAL。取出對應的CallbackQueue。在schduleTraversals調用postCallback方法時傳入的第二個參數(shù)mTraversalRunnable,就保存在上述代碼的某一個CallbackRecord中,查看CallbackRecord的run方法,其實就是調用了封裝在其中的action的run方法。從以上分析,這里的action,就是在mTraversalRunnable。分析到這一步,就可以回到ViewRootImpl的代碼中了。

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

mTraversalRunnable的run方法,調用了doTraversal方法。到此,熟悉View的繪制過程的同學,應該就很清楚接下來的流程了。doTraversal內部會調用performTraversals方法。在performTraversals方法中會,執(zhí)行當前頁面窗口對應的DecorView的整個視圖層級的繪制流程。

//ViewRootImpl.java
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

總結

本文主要介紹了

  • 卡頓和丟幀的概念
  • UI渲染機制的原理,其中涉及到的重要組件是Canvas、Surface、Graphic Buffer、和SurfaceFlinger。
  • 軟件繪制和硬件加速繪制的概念和區(qū)別。CPU和GPU在繪制渲染過程中,各自完成的工作。以及Skia庫和OpenGL ES庫的簡單介紹。
  • Project Butter的三要素,VSYNC信號和Triple Buffer的原理,著重從源碼角度分析了Choreographer的工作機制。

本文參考
Android開發(fā)高手課 UI 優(yōu)化(上):UI 渲染的幾個關鍵概念
Android 屏幕刷新機制

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374