Android View 绘制流程 ( 一 ) Measure

首先思考一个问题
public class V8Activity extends Activity {
    private String TAG = "V8Activity";
    ActivityV8Binding v8Binding;
    
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initBinding();
        //Log-1
        Log.d(TAG, "Button Height onCreate:" + v8Binding.btn.getMeasuredHeight());
        v8Binding.btn.post(new Runnable() {
            @Override
            public void run() {
                //Log-2
                Log.d(TAG, "Button Height post:" + v8Binding.btn.getMeasuredHeight());
            }
        });
    }

    private void initBinding() {
        v8Binding = DataBindingUtil.setContentView(this, R.layout.activity_v8);
    }

    protected void onResume() {
        super.onResume();
        //Log-3
        Log.d(TAG, "Button Height onResume:" + v8Binding.btn.getMeasuredHeight());
    }
}

Log - 1 打印值为多少 ?
Log - 2 打印值为多少 ?
Log - 3 打印值为多少 ?

运行结果 -> 「Log1 = 0」「Log2 = 132」「Log3 = 0」
为什么会产生这样的结果呢?下面一个一个来分析

Log1 打印 0 原因

之前分析了 setContentView 源码,setContentView 是将布局加载到 DecorView 中,这个时候 View 还没有测量,所以 Log1 打印为 0。

setContentView 源码解析

Log3 为什么打印 0

追一下源码
ActivityThread -> handleResumeActivity
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
    ...
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
    ...
    wm.addView(decor, l);
    ...
}

performResumeActivity() -> ActivityThread. performResumeActivity()

ActivityThread -> performResumeActivity
public ActivityClientRecord performResumeActivity(IBinder token, boolean finalStateRequest,
            String reason) {
    ...
    r.activity.performResume(r.startsNotResumed, reason);
    ...
}

r.activity.performResume() -> Activity.performResume()

Activity -> performResume
final void performResume(boolean followedByPause, String reason) {
    ...
    mInstrumentation.callActivityOnResume(this);
    ...
}

mInstrumentation.callActivityOnResume() -> Instrumentation.callActivityOnResume()

Instrumentation -> callActivityOnResume
public void callActivityOnResume(Activity activity) {
    activity.mResumed = true;
    activity.onResume();
        
    if (mActivityMonitors != null) {
        synchronized (mSync) {
            final int N = mActivityMonitors.size();
            for (int i=0; i<N; i++) {
                final ActivityMonitor am = mActivityMonitors.get(i);
                am.match(activity, activity, activity.getIntent());
            }
        }
    }
}

最后调用到 Activity 的 onResume 方法

handleResumeActivity 执行过程
  1. performResumeActivity
  2. wm.addView
  1. Activity 执行 onResume
  2. 将 DecorView 添加到 WindowManager
View 的绘制流程在 wm.addView 才开始,View 的绘制流程是在 Activity onResume 之后

Log2 打印数值

view.post() 入口
/**
 * <p>Causes the Runnable to be added to the message queue.
 * The runnable will be run on the user interface thread.</p>
 *
 * @param action The Runnable that will be executed.
 *
 * @return Returns true if the Runnable was successfully placed in to the
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.
 *
 * @see #postDelayed
 * @see #removeCallbacks
 */
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

The runnable will be run on the user interface thread -> runnable 会在主线程运行

很明显,这里的 attachInfo 并没有赋值,所以会走 getRunQueue().post() 方法

getRunQueue()
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

获取了 HandlerActionQueue 对象

HandlerActionQueue -> post
public void post(Runnable action) {
    postDelayed(action, 0);
}

public void postDelayed(Runnable action, long delayMillis) {
    final HandlerAction handlerAction = new HandlerAction(action, delayMillis);

    synchronized (this) {
        if (mActions == null) {
            mActions = new HandlerAction[4];
        }
        mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
        mCount++;
    }
}

将 runnable 转换成 HandlerAction 对象,然后缓存起来,大致可以理解为
HandlerAction 是 runnable 的一个封装类。

这里只是将消息缓存,并没有执行。
所以带着 2 点疑问,什么时候会执行 run 方法 ?在哪个位置会触发执行的方法 ?

什么时候会执行 run 方法

getRunQueue() 方法是在 View 中执行的,在 View 中搜索就可找到 dispatchAttachedToWindow 方法

dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    ...
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    ...
}

mAttachInfo 在 dispatchAttachedToWindow 中才初始化

mRunQueue -> executeActions
public void executeActions(Handler handler) {
    synchronized (this) {
        final HandlerAction[] actions = mActions;
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[I];
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        mActions = null;
        mCount = 0;
    }
}

解答上面的 2 点疑问,executeActions 会执行 run 方法( 通过 handler ),dispatchAttachedToWindow 会执行 executeActions 方法。

接着 View 的绘制流程 wm.addView() 往下看

wm -> Activity.getWindowManager() -> Window.getWindowManager() -> WindowManagerImpl
wm.addView() -> WindowManagerImpl.addView() -> WindowManagerGlobal.addView()

WindowManagerGlobal -> addView
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    root.setView(view, wparams, panelParentView, userId);
    ...
}
ViewRootImpl
public ViewRootImpl(Context context, Display display) {
    this(context, display, WindowManagerGlobal.getWindowSession(),
            false /* useSfChoreographer */);
}

public ViewRootImpl(Context context, Display display, IWindowSession session) {
    this(context, display, session, false /* useSfChoreographer */);
}

public ViewRootImpl(Context context, Display display, IWindowSession session,
            boolean useSfChoreographer) {
    ...
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
                context);
    ...
}

在 ViewRootImpl 的构造方法中初始化了 AttachInfo

root.setView() -> ViewRootImpl.setView()

ViewRootImpl -> setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
    ...
    requestLayout();
    ...
}
ViewRootImpl -> requestLayout
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
ViewRootImpl -> scheduleTraversals
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
mTraversalRunnable -> run
final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
ViewRootImpl -> doTraversal
void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

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

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}
ViewRootImpl -> performTraversals
private void performTraversals() {
    final View host = mView;
    ...
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    getRunQueue().executeActions(mAttachInfo.mHandler);
    ...
    performMeasure();
    performLayout();
    performDraw();
    ...
}

wm.addView(DecorView) -> windowManagerGlobal.addView(DecorView) ->
root.setView(DecorView)
所以这里的 host 其实是 DecorView

host.dispatchAttachedToWindow() -> DecorView.dispatchAttachedToWindow(),但是 DecorView 并没有实现这个方法,接着往上走,最终在 ViewGroup 中找到 dispatchAttachedToWindow 方法。

ViewGroup -> dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    super.dispatchAttachedToWindow(info, visibility);
    mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;

    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        final View child = children[I];
        child.dispatchAttachedToWindow(info,
                combineVisibility(visibility, child.getVisibility()));
    }
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    for (int i = 0; i < transientCount; ++i) {
        View view = mTransientViews.get(i);
        view.dispatchAttachedToWindow(info,
                combineVisibility(visibility, view.getVisibility()));
    }
}

ViewGroup 的 dispatchAttachedToWindow 方法会便利子 View 的 dispatchAttachedToWindow,而持有的 AttachInfo 对象是同一个,而 AttachInfo 持有的是 ViewRootImpl 中的 mHandler 对象,所以可以得出一个结论 executeActions 中的消息都是由 ViewRootImpl 的 mHandler 对象统一处理的。

ViewRootImpl -> performTraversals
private void performTraversals() {
    final View host = mView;
    ...
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    getRunQueue().executeActions(mAttachInfo.mHandler);
    ...
    //View 的绘制流程
    performMeasure();
    performLayout();
    performDraw();
    ...
}

接着看 performTraversals 方法

View.post() 能拿到数值,而 dispatchAttachedToWindow 方法不是在 View 的绘制流程之前调用的吗 ?这个时候我们可以用结果推断一下,View.post() 应该是在测量流程结束之后才执行的。
这个时候需要了解 Android 的消息机制。
void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

public void executeActions(Handler handler) {
    synchronized (this) {
        final HandlerAction[] actions = mActions;
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[I];
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }

        mActions = null;
        mCount = 0;
    }
}

handler 先发送的 mTraversalRunnable ,之后才发送的 handlerAction.action (也就是我们 Activity 里的 runnable ),所以会在 View 的绘制流程结束之后执行我们 Activity 中的 runnable。

模仿源码写了一个小例子,更容易理解绘制流程这一块
public class V8Test1 {
    private Context context;
    private V8Test2.AttachInfoTest attachInfoTest;
    private V8Test2 mTest;
    
    Handler handler = new Handler();
    public V8Test1(Context context) {
        this.context = context;
        attachInfoTest = new V8Test2.AttachInfoTest(context,handler);
    }

    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            Log.d("response","Test1 执行=====");
            performTraversals();
        }
    };

    void scheduleTraversals() {
        handler.post(runnable);
    }
    
    private void performTraversals() {
        final V8Test2 host = mTest;
        host.dispatchAttachedToWindow(attachInfoTest);
        Log.d("response","Test1 执行 Measure");
        Log.d("response","Test1 执行 Layout");
        Log.d("response","Test1 执行 draw");
    }

    public void setTest(V8Test2 mTest) {
        this.mTest = mTest;
    }
}

V8Test1 类对应就是 ViewRootImpl

public class V8Test2 {
    AttachInfoTest attachInfoTest;
    final static class AttachInfoTest {

        final Handler handler;
        final Context context;

        AttachInfoTest(Context mContext, Handler mHandler) {
            context = mContext;
            handler = mHandler;
        }

    }
    
    void dispatchAttachedToWindow(AttachInfoTest info) {
        this.attachInfoTest = info;
        executeActions(attachInfoTest.handler);
    }

    public void executeActions(Handler handler) {
        synchronized (this) {
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    Log.d("response","Test2执行=====");
                }
            }, 0);
        }
    }
}

V8Test2 类对应就是 View,其中的 executeActions 对应 HandlerActionQueue 的 executeActions 方法

测试代码
V8Test1 v8Test1 = new V8Test1(this);
v8Test1.setTest(new V8Test2());
v8Test1.scheduleTraversals();

new V8Test1 -> 对应 windowManagerGlobal.addView() 方法中的 new ViewRootImpl

v8Test1.setTest -> 对应 windowManagerGlobal.addView() 方法中的 root.setView()

v8Test1.scheduleTraversals 对应 ViewRootImpl 中的 scheduleTraversals

测试结果
response: Test1 执行=====
response: Test1 执行 Measure
response: Test1 执行 Layout
response: Test1 执行 draw
response: Test2执行=====

测试结果和我们预测的结果一致。

Github 源码链接

接下来就是 View 绘制流程的第一步 measure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

mView.measure() -> View.measure()

View -> measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}
View -> onMeasure (这里我们以 LinearLayout 为例子)
public class LinearLayout extends ViewGroup {
    ...
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
    ...
}

根据方向处理不同的方法,我们看 measureVertical 方法

LinearLayout -> measureVertical
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    for (int i = 0; i < count; ++i) {
        ...
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
        ...
    }
    ...
}
LinearLayout -> measureChildBeforeLayout
void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}
LinearLayout -> measureChildWithMargins
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

调用 child.measure 方法,如果 child 为 LinearLayout,则继续进入 LinearLayout 的 onMeasure 方法。

childWidthMeasureSpec, childHeightMeasureSpec 这 2 个参数分别对象宽高,下面记录一下测量模式的计算 -> getChildMeasureSpec()

measure 流程
ViewGroup -> getChildMeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    
    int size = Math.max(0, specSize - padding);
    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}

这里拿的是父布局的 Mode 和 size,然后根据父布局的 mode 去决定子 View 的 mode 和 size。所以子 View 的 mode 和 size 是由子 View 和父布局共同决定的。一个一个分析

当父布局 Mode 为 MeasureSpec.EXACTLY ( match_parent,100dp ) 时

子 View childSize > 0,子 View mode = EXACTLY,size = childSize

子 View childSize = MATCH_PARENT,子 View mode = EXACTLY,size = parentSize

子 View childSize = WRAP_CONTENT,子 View mode = AT_MOST,size = parentSize

当父布局 Mode 为 MeasureSpec. AT_MOST ( wrap_content ) 时

子 View childSize > 0,子 View mode = EXACTLY,size = childSize

子 View childSize = MATCH_PARENT,子 View mode = AT_MOST,size = parentSize

子 View childSize = WRAP_CONTENT,子 View mode = AT_MOST,size = parentSize

当父布局 Mode 为 MeasureSpec. UNSPECIFIED ( 基本不会用到 ) 时

子 View childSize > 0,子 View mode = EXACTLY,size = childSize

子 View childSize = MATCH_PARENT,子 View mode = UNSPECIFIED,size = parentSize

子 View childSize = WRAP_CONTENT,子 View mode = UNSPECIFIED,size = parentSize

测量模式

确定子 View 的 mode 和 size。

确定子 View 的 mode 和 size 之后,LinearLayout 循环会叠加每个子 View 的高度,最终通过 setMeasuredDimension 方法确定宽高。
LinearLayout -> measureVertical
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
    ...
}
如果是自定义View 就需要重写 onMeasure 方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

如果 onMeasure 方法没有设置宽高,那么会调用 getDefaultSize 设置一个默认的宽高

总结

Measure 流程是确定 View 的大小,如果有子 View 会先确定子 View 的大小,最后通过子 View 的大小确定父容器的大小。

到这里 View 绘制流程 Measure 流程 到这里就介绍完了,如果有什么写得不对的,可以在下方评论留言,我会第一时间改正。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容