view系列源碼分析之Choreographer機制和卡頓優(yōu)化

Choreographer 是 Android 4.1 google的黃油計劃新增的機制馍佑,用于配合系統(tǒng)的 VSYNC 中斷信號混巧。其主要用途是接收系統(tǒng)的 VSYNC 信號姜挺,統(tǒng)一管理應(yīng)用的輸入盐类、動畫和繪制等任務(wù)的執(zhí)行時機晰绎。也就是(CALLBACK_INPUT,CALLBACK_ANIMATION,CALLBACK_TRAVERSAL,CALLBACK_COMMIT)寓落,而我們主要的用途就是來查看app的幀率情況,下面來具體分析下這個神秘的類
當(dāng)我們進行invalidate或者requestLayout時荞下,總會執(zhí)行viewRootImp的scheduleTraversals方法

  void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

而這個mTraversalRunnable就是我們所要執(zhí)行的任務(wù)了伶选,那究竟是何時執(zhí)行呢?
首先會mChoreographer.postCallback會間接調(diào)用postCallbackDelayedInternal方法

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

我們可以看到正常情況下會執(zhí)行scheduleFrameLocked方法

private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            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);
            }
        }
    }

由于在4.1上是使用VSYNC信號的尖昏,所以就自然會調(diào)用scheduleVsyncLocked方法,會間接調(diào)用scheduleVsync方法

**
     * Schedules a single vertical sync pulse to be delivered when the next
     * display frame begins.
     */
    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);
        }
    }

注意:這里的注釋說的很清楚了仰税,當(dāng)下一幀來臨時準(zhǔn)備一個要分發(fā)的垂直同步信號,啥意思呢抽诉?簡單來說就是當(dāng)調(diào)用了nativeScheduleVsync方法時陨簇,當(dāng)屏幕需要刷新的時候,也就是每隔16.6ms會通過native的looper分發(fā)到j(luò)ava層迹淌,從而調(diào)用java的方法河绽,那是哪個方法呢己单?

  // Called from native code.
    @SuppressWarnings("unused")
    private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
        onVsync(timestampNanos, builtInDisplayId, frame);
    }

很明顯是此方法
舉個例子,比如屏幕顯示的是第一幀耙饰,你在第一幀調(diào)用invalidate纹笼,其實并不是立即刷新的,而是在一幀會去注冊一個Vsync(前提是這一幀cpu空閑情況下)苟跪,當(dāng)下一幀來臨時也就是第二幀的時候會調(diào)用dispatchVsync此方法允乐,當(dāng)然這是一種比較簡單的情況,復(fù)雜的等會說
那么來看一下調(diào)用的onVsync方法

  public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
            // Ignore vsync from secondary display.
            // This can be problematic because the call to scheduleVsync() is a one-shot.
            // We need to ensure that we will still receive the vsync from the primary
            // display which is the one we really care about.  Ideally we should schedule
            // vsync for a particular display.
            // At this time Surface Flinger won't send us vsyncs for secondary displays
            // but that could change in the future so let's log a message to help us remember
            // that we need to fix this.
            if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                Log.d(TAG, "Received vsync from secondary display, but we don't support "
                        + "this case yet.  Choreographer needs a way to explicitly request "
                        + "vsync for a specific display to ensure it doesn't lose track "
                        + "of its scheduled vsync.");
                scheduleVsync();
                return;
            }

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

注意看下timestampNanos的參數(shù)(簡單來說就是從調(diào)用native方法以后到回調(diào)到這個方法所經(jīng)過的的時間)
接著看會發(fā)送一條異步消息削咆,簡單來說就是此消息在消息隊列中不用排隊牍疏,可以最先被取出來,很明顯拨齐,會調(diào)用下面的run方法進行處理

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

這里調(diào)用的doframe方法

void doFrame(long frameTimeNanos, int frame) {
        final long startNanos;
        synchronized (mLock) {
            if (!mFrameScheduled) {
                return; // no work to do
            }

            if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                mDebugPrintNextFrameTimeDelta = false;
                Log.d(TAG, "Frame time delta: "
                        + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
            }

            long intendedFrameTimeNanos = frameTimeNanos;
            startNanos = System.nanoTime();
            final long jitterNanos = startNanos - frameTimeNanos;
            if (jitterNanos >= mFrameIntervalNanos) {
                final long skippedFrames = jitterNanos / mFrameIntervalNanos;
                if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                    Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                            + "The application may be doing too much work on its main thread.");
                }
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                            + "which is more than the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Skipping " + skippedFrames + " frames and setting frame "
                            + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
                }
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                if (DEBUG_JANK) {
                    Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                            + "previously skipped frame.  Waiting for next vsync.");
                }
                scheduleVsyncLocked();
                return;
            }

            if (mFPSDivisor > 1) {
                long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
                if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                    scheduleVsyncLocked();
                    return;
                }
            }

            mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
            mFrameScheduled = false;
            mLastFrameTimeNanos = frameTimeNanos;
        }

        try {
            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);

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

            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

        if (DEBUG_FRAMES) {
            final long endNanos = System.nanoTime();
            Log.d(TAG, "Frame " + frame + ": Finished, took "
                    + (endNanos - startNanos) * 0.000001f + " ms, latency "
                    + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
        }
    }

有幾個重點的地方要說下鳞陨,我們開發(fā)時偶爾在log上會看到Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread."這句話,很多人以為出現(xiàn)這句話是因為ui線程太耗時了瞻惋,其實仔細(xì)想想就知道是錯的厦滤,因為在回掉這方法的時候根本沒有執(zhí)行到
測量,繪制等歼狼,它的兩個時間對比的是回掉到此幀的時間frameTimeNanos和當(dāng)前的時間掏导,如果大于16.66ms就會打印這句話,那就是說執(zhí)行native方法到onFrame回掉超過16.66ms羽峰,而onFrame回掉是通過異步消息趟咆,可以忽略不計,那唯一可能出現(xiàn)的情況就是通過handler后執(zhí)行dispatchVsync方法梅屉,與執(zhí)行native方法的耗時值纱,也就是說此時會有多個message,而執(zhí)行dispatchVsync方法的message是排在比較后面的坯汤,這也解釋了這句log:he application may be doing too much work on its main thread.

所以這句話并不能判斷是ui卡頓了虐唠,只能說明有很多message,要減少不必要的message才是優(yōu)化的根本惰聂。
而frameTimeNanos = startNanos - lastFrameOffset;簡單來說就是給vsnc設(shè)置幀數(shù)的偏移量
那又是啥意思呢疆偿?
比如我在第一幀發(fā)起了重繪制,按理來說第二幀就會收到Vsync的信號值搓幌,但是由于message阻塞了超過了16.66杆故,所以收到Vsync的信號自然延續(xù)要了第三幀。
在了解這句話以后鼻种,接下來就是回調(diào)繪制反番,或者input事件了,可以看到代碼會間接調(diào)用doCallbacks

  void doCallbacks(int callbackType, long frameTimeNanos) {
        CallbackRecord callbacks;
        synchronized (mLock) {
            // We use "now" to determine when callbacks become due because it's possible
            // for earlier processing phases in a frame to post callbacks that should run
            // in a following phase, such as an input event that causes an animation to start.
            final long now = System.nanoTime();
            callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
            if (callbacks == null) {
                return;
            }
            mCallbacksRunning = true;

            // Update the frame time if necessary when committing the frame.
            // We only update the frame time if we are more than 2 frames late reaching
            // the commit phase.  This ensures that the frame time which is observed by the
            // callbacks will always increase from one frame to the next and never repeat.
            // We never want the next frame's starting frame time to end up being less than
            // or equal to the previous frame's commit frame time.  Keep in mind that the
            // next frame has most likely already been scheduled by now so we play it
            // safe by ensuring the commit time is always at least one frame behind.
            if (callbackType == Choreographer.CALLBACK_COMMIT) {
                final long jitterNanos = now - frameTimeNanos;
                Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
                if (jitterNanos >= 2 * mFrameIntervalNanos) {
                    final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
                            + mFrameIntervalNanos;
                    if (DEBUG_JANK) {
                        Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
                                + " ms which is more than twice the frame interval of "
                                + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                                + "Setting frame time to " + (lastFrameOffset * 0.000001f)
                                + " ms in the past.");
                        mDebugPrintNextFrameTimeDelta = true;
                    }
                    frameTimeNanos = now - lastFrameOffset;
                    mLastFrameTimeNanos = frameTimeNanos;
                }
            }
        }
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
            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);
        }
    }

從而執(zhí)行 c.run(frameTimeNanos);方法進行回調(diào)
這里放張圖叉钥,大家可以理解一下


1858589-5292b2854f3dc385.jpg

簡單說下
一開始注冊了vsync信號罢缸,所以在下一幀調(diào)用了dispatchVsync方法,由于沒有message阻塞投队,所以接收到了此幀的信號枫疆,進行了繪制,在繪制完成后又注冊了信號敷鸦,可以看到一幀內(nèi)注冊同一信號是無效的息楔,但是回掉會執(zhí)行,到了下一幀扒披,由于message的超時不到16.66ms值依,所以也就是執(zhí)行dispatchVsync與執(zhí)行native方法的間隔時間,所以還是此幀還是有信號的碟案,而由于此幀耗時超過了一幀愿险,所以沒有注冊Vsync,當(dāng)然也不會執(zhí)行dispatchVsync方法价说,到了最后可以看到由于message超過了16.66即使在第三幀注冊了Vsync信號辆亏,但是dispatchVsync執(zhí)行的事件已經(jīng)到了第5幀

卡頓優(yōu)化

在簡單分析完了Choreographer機制以后,來具體說下卡頓優(yōu)化的兩種方案的原理
1鳖目、 利用UI線程的Looper打印的日志匹配扮叨;

2、 使用Choreographer.FrameCallback

第一種是blockcanary的原理领迈,就是利用looper.loop分發(fā)事件的時間間隔作為卡頓的依據(jù)

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

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        boolean slowDeliveryDetected = false;

        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;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
...

也就是對logging進行深入的研究彻磁,一般超過了1000ms就認(rèn)為卡頓了,但我們有沒有想過狸捅,我們通常說的卡頓不是說超過了16.66ms么兵迅,為何這里要超過500ms,甚至1000ms才算是卡頓?
我們要知道薪贫,android系統(tǒng)所有的執(zhí)行都是基于looper機制的恍箭,也就是所有的消息執(zhí)行的時間超過1000ms就認(rèn)定卡頓了,舉個例子瞧省,我們可能在主線程操作數(shù)據(jù)庫扯夭,可能在主線程解析json,可能在主線程寫文件鞍匾,可能在主線程做一些例如高斯模糊的耗時操作
這種情況下我們利用blockcanary是可以檢測出來的交洗,但是如果是卡頓呢?當(dāng)然我們也可以把時間設(shè)定為50ms橡淑,但是檢測出來的太多了构拳,所以就需要第二個機制了Choreographer.FrameCallback,通常這樣寫

public class BlockDetectByChoreographer {
    public static void start() {
        Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { 
                   long lastFrameTimeNanos = 0; 
                   long currentFrameTimeNanos = 0;

                @Override
                public void doFrame(long frameTimeNanos) { 
                    if(lastFrameTimeNanos == 0){
                        lastFrameTimeNanos == frameTimeNanos;
                    }
                    currentFrameTimeNanos = frameTimeNanos;
                    long diffMs = TimeUnit.MILLISECONDS.convert(currentFrameTimeNanos-lastFrameTimeNanos, TimeUnit.NANOSECONDS);
                    if (diffMs > 16.6f) {            
                       long droppedCount = (int)diffMs / 16.6;
                    }
                        if (LogMonitor.getInstance().isMonitor()) {
                        LogMonitor.getInstance().removeMonitor();                    
                    } 
                    LogMonitor.getInstance().startMonitor();
                    Choreographer.getInstance().postFrameCallback(this);
                }
        });
    }
}

我們可以看到調(diào)用了postFrameCallback方法,會間接調(diào)用postFrameCallbackDelayed

  public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }

可以看到這里是CALLBACK_ANIMATION的callback,也就說只要監(jiān)聽了此方法他就會不斷的調(diào)用doFrame置森,在doframe里調(diào)用postFrameCallback斗埂,從而來達(dá)到完美的監(jiān)聽ui卡頓的效果
也就是onMeasure,onLayout,onDraw的耗時時間

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市凫海,隨后出現(xiàn)的幾起案子呛凶,更是在濱河造成了極大的恐慌,老刑警劉巖行贪,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漾稀,死亡現(xiàn)場離奇詭異,居然都是意外死亡建瘫,警方通過查閱死者的電腦和手機崭捍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來啰脚,“玉大人殷蛇,你說我怎么就攤上這事〖鸩ィ” “怎么了晾咪?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長贮配。 經(jīng)常有香客問我谍倦,道長,這世上最難降的妖魔是什么泪勒? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任昼蛀,我火速辦了婚禮,結(jié)果婚禮上圆存,老公的妹妹穿的比我還像新娘叼旋。我一直安慰自己,他們只是感情好沦辙,可當(dāng)我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布夫植。 她就那樣靜靜地躺著,像睡著了一般油讯。 火紅的嫁衣襯著肌膚如雪详民。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天陌兑,我揣著相機與錄音沈跨,去河邊找鬼。 笑死兔综,一個胖子當(dāng)著我的面吹牛饿凛,可吹牛的內(nèi)容都是我干的狞玛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼涧窒,長吁一口氣:“原來是場噩夢啊……” “哼心肪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起杀狡,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蒙畴,失蹤者是張志新(化名)和其女友劉穎贰镣,沒想到半個月后呜象,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡碑隆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年恭陡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片上煤。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡休玩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出劫狠,到底是詐尸還是另有隱情拴疤,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布独泞,位于F島的核電站呐矾,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏懦砂。R本人自食惡果不足惜蜒犯,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望荞膘。 院中可真熱鬧罚随,春花似錦、人聲如沸羽资。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屠升。三九已至潮改,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弥激,已是汗流浹背进陡。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留微服,地道東北人趾疚。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓缨历,卻偏偏與公主長得像,于是被迫代替她去往敵國和親糙麦。 傳聞我的和親對象是個殘疾皇子辛孵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,724評論 2 354

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