Android 布局优化之 ViewStub、include、merge 使用 与源码分析

一、include

首先用得最多的应该是 include,按照官方的意思,

include 就是为了解决重复

定义相同布局的问题。例如你有五个界面,这五个界面的顶部都有布局一模一样

的一个返回按钮和一个文本控件,在不使用 include 的情况下你在每个界面都需

要重新在 xml 里面写同样的返回按钮和文本控件的顶部栏,这样的重复工作会

相当的恶心。使用 include 标签,我们只需要把这个会被多次使用的顶部栏独立

成一个 xml 文件,然后在需要使用的地方通过 include 标签引入即可。其实就

相当于 C 语言、C++中的 include 头文件一样,我们把一些常用的、底层的 API

封装起来,然后复用,需要的时候引入它即可,而不必每次都自己写一遍。示例

如下 :

my_title_layout.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:id="@+id/my_title_parent_id"

android:layout_height="wrap_content" >

<ImageButton

android:id="@+id/back_btn"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:src="@drawable/ic_launcher" />

<TextView

android:id="@+id/title_tv"

android:layout_width="wrap_content"

android:layout_height="wrap_content"android:layout_centerVertical="true"

android:layout_marginLeft="20dp"

android:layout_toRightOf="@+id/back_btn"

android:gravity="center"

android:text="我的 title"

android:textSize="18sp" />

</RelativeLayout>

include 布局文件:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical" >

<include

android:id="@+id/my_title_ly"

android:layout_width="match_parent"

android:layout_height="wrap_content"

layout="@layout/my_title_layout" />

<!-- 代码省略 -->

</LinearLayout>

这样我们就可以使用 my_title_layout 了。

注意事项使用 include 最常见的问题就是 findViewById 查找不到目标控件,这个

问题出现的前提是在 include 时设置了 id,而在 findViewById 时却用了被

include 进来的布局的根元素 id。例如上述例子中,

include 时设置了该布

局 的 id 为 my_title_ly , 而 my_title_layout.xml 中 的 根 视 图 的 id 为

my_title_parent_id。此时如果通过 findViewById 来找 my_title_parent_id

这个控件,然后再查找 my_title_parent_id 下的子控件则会抛出空指针。

代码如下 :

View titleView = findViewById(R.id.my_title_parent_id) ;

// 此时 titleView 为空,找不到。此时空指针

TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;

titleTextView.setText("new Title");

其正确的使用形式应该如下:

// 使用 include 时设置的 id,即 R.id.my_title_ly

View titleView = findViewById(R.id.my_title_ly) ;

// 通过 titleView 找子控件

TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ;

titleTextView.setText("new Title");

或者更简单的直接查找它的子控件:

TextView titleTextView = (TextView)findViewById(R.id.title_tv) ;

titleTextView.setText("new Title");那么使用 findViewById(R.id.my_title_parent_id)为什么会报空指针呢? 我们

来分析它的源码看看吧。对于布局文件的解析,最终都会调用到 LayoutInflater

的 inflate 方法,该方法最终又会调用 rInflate 方法,我们看看这个方法。

/**

* Recursive method used to descend down the xml hierarchy and

instantiate

* views, instantiate their children, and then call onFinishInflate().

*/

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

final int depth = parser.getDepth();

int type;

// 迭代 xml 中的所有元素,挨个解析

while (((type = parser.next()) != XmlPullParser.END_TAG ||

parser.getDepth()>depth)&&type ! =XmlPullParser.END_DOCUMENT) {

if (type != XmlPullParser.START_TAG) {

continue;

}

final String name = parser.getName();

if (TAG_REQUEST_FOCUS.equals(name)) {

parseRequestFocus(parser, parent);

} else if (TAG_INCLUDE.equals(name)) {

// 如果 xml 中的节点是include 节点,则调用 parseInclude 方法

if (parser.getDepth() == 0) {

throw new InflateException("<include /> cannot be

the root element");

}

parseInclude(parser, parent, attrs);

} else if (TAG_MERGE.equals(name)) {

throw new InflateException("<merge /> must be the

root element");

} else if (TAG_1995.equals(name)) {

final View view = new BlinkLayout(mContext, attrs);

final ViewGroup viewGroup = (ViewGroup) parent;

final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);

rInflate(parser, view, attrs, true);viewGroup.addView(view, params);

} else {

final View view = createViewFromTag(parent, name, attrs);

final ViewGroup viewGroup = (ViewGroup) parent;

final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);

rInflate(parser, view, attrs, true);

viewGroup.addView(view, params);

}

}

if (finishInflate) parent.onFinishInflate();

}

这个方法其实就是遍历 xml 中的所有元素,然后挨个进行解析。例如解析

到一个标签,那么就根据用户设置的一些 layout_width、layout_height、id 等

属性来构造一个 TextView 对象,然后添加到父控件(ViewGroup 类型)中。标签

也是一样的,我们看到遇到 include 标签时,会调用 parseInclude 函数,这就

是对标签的解析,我们看看吧。

private void parseInclude(XmlPullParser parser, View parent, AttributeSet

attrs)

throws XmlPullParserException, IOException {int type;

if (parent instanceof ViewGroup) {

final int layout = attrs.getAttributeResourceValue(null,

"layout", 0);

if (layout == 0) {// include 标签中没有设置 layout 属性,会抛出

异常

final String value = attrs.getAttributeValue(null, "layout");

if (value == null) {

throw new InflateException("You must specifiy a layout in the" + " include tag: <include layout=\"@layout/layoutID\" />");

} else {

throw new InflateException("You must specifiy a valid layout " + "reference. The layout ID " + value + " is not valid.");

}

} else {

final XmlResourceParser childParser =getContext().getResources().getLayout(layout);

try {// 获取属性集,即在 include 标签中设置的属性

final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

while((type=childParser.next())!=XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {

// Empty.

}

if (type != XmlPullParser.START_TAG) {

throw new InflateException(childParser.getPositionDescription() + ": No start tag found!");

}

// 1、解析 include 中的第一个元素

final String childName = childParser.getName();

// 如果第一个元素是 merge 标签,那么调用 rInflate 函

数解析

if (TAG_MERGE.equals(childName)) {// Inflate all children.

rInflate(childParser, parent, childAttrs, false);

} else {// 2、我们例子中的情况会走到这一步,首先根据

include 的属性集创建被 include 进来的 xml 布局的根 view

// 这里的根 view 对应为 my_title_layout.xml 中的

RelativeLayout

final View view = createViewFromTag(parent,

childName, childAttrs);

final ViewGroup group = (ViewGroup) parent;//

include 标签的 parent view

ViewGroup.LayoutParams params = null;

try {// 获 3、取布局属性

params= group.generateLayoutParams(attrs);

} catch (RuntimeException e) {

params = group.generateLayoutParams(childAttrs);

} finally {

if (params != null) {// 被 inlcude 进 来 的 根

view 设置布局参数

view.setLayoutParams(params);}

}

// 4、Inflate all children. 解析所有子控件

rInflate(childParser, view, childAttrs, true);

// Attempt to override the included layout's

android:id with the

// one set on the <include /> tag itself.

TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, 0, 0); 

int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);

// While we're at it, let's try to override

android:visibility.int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1); 

a.recycle();

// 5、将 include 中设置的 id 设置给根 view,因此实

际上 my_title_layout.xml 中的 RelativeLayout 的 id 会变成 include 标签中的 id,

include 不设置 id,那么也可以通过 relative 的找到.

if (id != View.NO_ID) {

view.setId(id);

}

switch (visibility) {

case 0:

view.setVisibility(View.VISIBLE);

break;

case 1:

view.setVisibility(View.INVISIBLE);

break;

case 2:

view.setVisibility(View.GONE);

break;

}

// 6、将根 view 添加到父控件中

group.addView(view);

}

} finally {

childParser.close();

}

}} else {

throw new InflateException("<include /> can only be used

inside of a ViewGroup");

}

final int currentDepth = parser.getDepth();

while (((type = parser.next()) != XmlPullParser.END_TAG ||

parser.getDepth()

> currentDepth)

&& type

!=

XmlPullParser.END_DOCUMENT) {

// Empty

}

}

整个过程就是根据不同的标签解析不同的元素,首先会解析 include 元素,然后

再解析被 include 进来的布局的 root view 元素。在我们的例子中对应的 root

view 就是 id 为 my_title_parent_id 的 RelativeLayout,然后再解析 root view

下面的所有元素,这个过程是从上面注释的 2~4 的过程,然后是设置布局参数。

我们注意看注释 5 处,这里就解释了为什么 include 标签和被引入的布局的根元

素都设置了 id 的情况下,通过被引入的根元素的 id 来查找子控件会找不到的情

况。我们看到,注释 5 处的会判断 include 标签的 id 如果不是 View.NO_ID 的

话会把该 id 设置给被引入的布局根元素的 id,即此时在我们的例子中被引入的

id 为 my_title_parent_id 的根元素 RelativeLayout 的 id 被设置成了 include

标签中的 id,即 RelativeLayout 的 id 被动态修改成了”my_title_ly”。因此此时我们再通过“my_title_parent_id”这个 id 来查找根元素就会找不到了!

所以结论就是: 如果 include 中设置了 id,那么就通过 include 的 id 来查找被

include 布局根元素的 View;如果 include 中没有设置 Id, 而被 include 的布

局的根元素设置了 id,那么通过该根元素的 id 来查找该 view 即可。拿到根元

素后查找其子控件都是一样的。

二、ViewStub

我们先看看官方的说明:

ViewStub is a lightweight view with no dimension and doesn’t draw

anything or participate in the layout. As such, it’s cheap to inflate and

cheap to leave in a view hierarchy. Each ViewStub simply needs to

include the android:layout attribute to specify the layout to inflate.

其实 ViewStub 就是一个宽高都为 0 的一个 View,它默认是不可见的,只有通

过调用 setVisibility 函数或者 Inflate 函数才会将其要装载的目标布局给加载出

来,从而达到延迟加载的效果,这个要被加载的布局通过 android:layout 属性

来设置。例如我们通过一个 ViewStub 来惰性加载一个消息流的评论列表,因为

一个帖子可能并没有评论,此时我可以不加载这个评论的 ListView,只有当有

评论时我才把它加载出来,这样就去除了加载 ListView 带来的资源消耗以及延

时,示例如下 :

<ViewStub

android:id="@+id/stub_import"

android:inflatedId="@+id/stub_comm_lv"android:layout="@layout/my_comment_layout"

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:layout_gravity="bottom" /

my_comment_layout.xml 如下:

<?xml version="1.0" encoding="utf-8"?>

<ListView

xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="match_parent"

android:id="@+id/my_comm_lv"

android:layout_height="match_parent" >

</ListView>

在运行时,我们只需要控制 id 为 stub_import 的 ViewStub 的可见性或者

调用 inflate()函数来控制是否加载这个评论列表即可。示例如下 :

public class MainActivity extends Activity {

public void onCreate(Bundle b){

// main.xml 中包含上面的 ViewStub

setContentView(R.layout.main);// 方式 1,获取 ViewStub,

ViewStub

listStub

=

(ViewStub)

findViewById(R.id.stub_import);

// 加载评论列表布局

listStub.setVisibility(View.VISIBLE);

// 获取到评论 ListView,注意这里是通过 ViewStub 的 inflatedId

来获取

ListView commLv = findViewById(R.id.stub_comm_lv);

if ( listStub.getVisibility() == View.VISIBLE ) {

// 已经加载, 否则还没有加载

}

}

}

通过 setVisibility(View.VISIBILITY)来加载评论列表,此时你要获取到评论

ListView 对象的话,则需要通过 findViewById 来查找,而这个 id 并不是就是

ViewStub 的 id。

这是为什么呢 ?

我们先看 ViewStub 的部分代码吧:

@SuppressWarnings({"UnusedDeclaration"})

public ViewStub(Context context, AttributeSet attrs, int defStyle){

TypedArray

a

=

context.obtainStyledAttributes(attrs,

com.android.internal.R.styleable.ViewStub,

defStyle, 0);

// 获取 inflatedId 属性

mInflatedId

=

a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);

mLayoutResource

=

a.getResourceId(R.styleable.ViewStub_layout, 0);

a.recycle();

a

=

context.obtainStyledAttributes(attrs,

com.android.internal.R.styleable.View, defStyle, 0);

mID = a.getResourceId(R.styleable.View_id, NO_ID);

a.recycle();

initialize(context);

}

private void initialize(Context context) {

mContext = context;setVisibility(GONE);// 设置不可教案

setWillNotDraw(true);// 设置不绘制

}

@Override

protected

void

onMeasure(int

widthMeasureSpec,

int

heightMeasureSpec) {

setMeasuredDimension(0, 0);// 宽高都为 0

}

@Override

public void setVisibility(int visibility) {

if (mInflatedViewRef != null) {// 如果已经加载过则只设置

Visibility 属性

View view = mInflatedViewRef.get();

if (view != null) {

view.setVisibility(visibility);

} else {

throw new IllegalStateException("setVisibility called

on un-referenced view");

}} else {// 如果未加载,这加载目标布局

super.setVisibility(visibility);

if (visibility == VISIBLE || visibility == INVISIBLE) {

inflate();// 调用 inflate 来加载目标布局

}

}

}

/**

* Inflates the

layout

resource identified

by

{@link

#getLayoutResource()}

* and replaces this StubbedView in its parent by the inflated

layout resource.

*

* @return The inflated layout resource.

*

*/

public View inflate() {

final ViewParent viewParent = getParent();

if (viewParent != null && viewParent instanceof ViewGroup)

{if (mLayoutResource != 0) {

final

ViewGroup

parent

=

(ViewGroup)

viewParent;// 获取 ViewStub 的 parent view,也是目标布局根元素的 parent

view

final

LayoutInflater

factory

=

LayoutInflater.from(mContext);

final View view = factory.inflate(mLayoutResource,

parent,

false);// 1、加载目标布局

// 2 、 如 果 ViewStub 的 inflatedId 不 是 NO_ID 则 把

inflatedId 设置为目标布局根元素的 id,即评论 ListView 的 id

if (mInflatedId != NO_ID) {

view.setId(mInflatedId);

}

final int index = parent.indexOfChild(this);

parent.removeViewInLayout(this);// 3、将 ViewStub

自身从 parent 中移除

final ViewGroup.LayoutParams layoutParams =

getLayoutParams();

if (layoutParams != null) {parent.addView(view, index, layoutParams);//

4、将目标布局的根元素添加到 parent 中,有参数

} else {

parent.addView(view, index);// 4、将目标布局的

根元素添加到 parent 中

}

mInflatedViewRef

=

new

WeakReference<View>(view);

if (mInflateListener != null) {

mInflateListener.onInflate(this, view);

}

return view;

} else {

throw new IllegalArgumentException("ViewStub

must have a valid layoutResource");

}

} else {

throw new IllegalStateException("ViewStub must have a

non-null ViewGroup viewParent");}

}

可以看到,其实最终加载目标布局的还是 inflate()函数,在该函数中将加载

目标布局,获取到根元素后,如果 mInflatedId 不为 NO_ID 则把 mInflatedId

设置为根元素的 id,这也是为什么我们在获取评论 ListView 时会使用

findViewById(R.id.stub_comm_lv) 来 获 取 , 其 中 的 stub_comm_lv 就 是

ViewStub 的 inflatedId。当然如果你没有设置 inflatedId 的话还是可以通过评

论列表的 id 来获取的,例如 findViewById(R.id.my_comm_lv)。然后就是

ViewStub 从 parent 中移除、把目标布局的根元素添加到 parent 中。最后会把

目标布局的根元素返回,因此我们在调用 inflate()函数时可以直接获得根元素,

省掉了 findViewById 的过程。

还有一种方式加载目标布局的就是直接调用 ViewStub 的 inflate()方法,示例如

下 :

public class MainActivity extends Activity {

// 把 commLv2 设置为类的成员变量

ListView commLv2 = null;

//

public void onCreate(Bundle b){

// main.xml 中包含上面的 ViewStub

setContentView(R.layout.main);// 方式二

ViewStub

listStub2

=

(ViewStub)

findViewById(R.id.stub_import) ;

// 成员变量 commLv2 为空则代表未加载

if ( commLv2 == null ) {

// 加载评论列表布局, 并且获取评论 ListView,inflate 函数直接返回

ListView 对象

commLv2 = (ListView)listStub2.inflate();

} else {

// ViewStub 已经加载

}

}

}

注意事项

1. 判断是否已经加载过, 如果通过 setVisibility 来加载,那么通过判断可

见性即可;如果通过 inflate()来加载是不可以通过判断可见性来处理的,

而需要使用方式 2 来进行判断。

2. findViewById 的问题,注意 ViewStub 中是否设置了 inflatedId,如果

设置了则需要通过 inflatedId 来查找目标布局的根元素。

三、Merge首先我们看官方的说明:

The tag helps eliminate redundant view groups in your view hierarchy

when including one layout within another. For example, if your main

layout is a vertical LinearLayout in which two consecutive views can be

re-used in multiple layouts, then the re-usable layout in which you place

the two views requires its own root view. However, using another

LinearLayout as the root for the re-usable layout would result in a vertical

LinearLayout inside a vertical LinearLayout. The nested LinearLayout

serves no real purpose other than to slow down your UI performance.

其实就是减少在 include 布局文件时的层级。标签是这几个标签中最让我费解

的,大家可能想不到,标签竟然会是一个 Activity,里面有一个 LinearLayout

对象。

/**

* Exercise <merge /> tag in XML files.

*/

public class Merge extends Activity {

private LinearLayout mLayout;

@Override

protected void onCreate(Bundle icicle) {

super.onCreate(icicle);mLayout = new LinearLayout(this);

mLayout.setOrientation(LinearLayout.VERTICAL);

LayoutInflater.from(this).inflate(R.layout.merge_tag, mLayout);

setContentView(mLayout);

}

public ViewGroup getLayout() {

return mLayout;

}

}

使用 merge 来组织子元素可以减少布局的层级。例如我们在复用一个含有多个

子控件的布局时,肯定需要一个 ViewGroup 来管理,例如这样 :

<FrameLayout

xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="fill_parent">

<ImageView

android:layout_width="fill_parent"

android:layout_height="fill_parent"android:scaleType="center"

android:src="@drawable/golden_gate" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginBottom="20dip"

android:layout_gravity="center_horizontal|bottom"

android:padding="12dip"

android:background="#AA000000"

android:textColor="#ffffffff"

android:text="Golden Gate" />

</FrameLayout>

使用 merge 标签就会消除上图中蓝色的 FrameLayout 层级。示例如下 :

<merge

xmlns:android="http://schemas.android.com/apk/res/android"><ImageView

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:scaleType="center"

android:src="@drawable/golden_gate" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginBottom="20dip"

android:layout_gravity="center_horizontal|bottom"

android:padding="12dip"

android:background="#AA000000"

android:textColor="#ffffffff"

android:text="Golden Gate" />

</merge>

那么它是如何实现的呢,我们还是看源码吧。相关的源码也是在 LayoutInflater的 inflate()函数中。

public View inflate(XmlPullParser parser, ViewGroup root, boolean

attachToRoot) {

synchronized (mConstructorArgs) {

final AttributeSet attrs = Xml.asAttributeSet(parser);

Context lastContext = (Context)mConstructorArgs[0];

mConstructorArgs[0] = mContext;

View result = root;

try {

// Look for the root node.

int type;

while ((type = parser.next()) != XmlPullParser.START_TAG

&&

type != XmlPullParser.END_DOCUMENT) {

// Empty

}

if (type != XmlPullParser.START_TAG) {

throw

new

InflateException(parser.getPositionDescription()

+ ": No start tag found!");}

final String name = parser.getName();

// m 如果是 erge 标签,那么调用 rInflate 进行解析

if (TAG_MERGE.equals(name)) {

if (root == null || !attachToRoot) {

throw new InflateException("<merge /> can be

used only with a valid "

+

"ViewGroup

root

and

attachToRoot=true");

}

// 解析 merge 标签

rInflate(parser, root, attrs, false);

} else {

// 代码省略

}

} catch (XmlPullParserException e) {

// 代码省略

}return result;

}

}

void rInflate(XmlPullParser parser, View parent, final AttributeSet

attrs,

boolean

finishInflate)

throws

XmlPullParserException,

IOException {

final int depth = parser.getDepth();

int type;

while (((type = parser.next()) != XmlPullParser.END_TAG ||

parser.getDepth()

>

depth)

&&

type

!=

XmlPullParser.END_DOCUMENT) {

if (type != XmlPullParser.START_TAG) {

continue;

}

final String name = parser.getName();if (TAG_REQUEST_FOCUS.equals(name)) {

parseRequestFocus(parser, parent);

} else if (TAG_INCLUDE.equals(name)) {

// 代码省略

parseInclude(parser, parent, attrs);

} else if (TAG_MERGE.equals(name)) {

throw new InflateException("<merge /> must be the root

element");

} else if (TAG_1995.equals(name)) {

final View view = new BlinkLayout(mContext, attrs);

final ViewGroup viewGroup = (ViewGroup) parent;

final

ViewGroup.LayoutParams

params

=

viewGroup.generateLayoutParams(attrs);

rInflate(parser, view, attrs, true);

viewGroup.addView(view, params);

} else { // 我们的例子会进入这里

final View view = createViewFromTag(parent, name,

attrs);

// 获取 merge 标签的 parent

final ViewGroup viewGroup = (ViewGroup) parent;

// 获取布局参数final

ViewGroup.LayoutParams

params

=

viewGroup.generateLayoutParams(attrs);

// 递归解析每个子元素

rInflate(parser, view, attrs, true);

// 将子元素直接添加到 merge 标签的 parent view 中

viewGroup.addView(view, params);

}

}

if (finishInflate) parent.onFinishInflate();

}

其实就是如果是 merge 标签,那么直接将其中的子元素添加到 merge 标签

parent 中,这样就保证了不会引入额外的层级。

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

推荐阅读更多精彩内容