LayoutInflater 详解(一)

LayoutInflater 是我们在 Android 开发中经常用到的一个类

为什么要说是经常用到的呢?因为我们写的 xml 布局配置文件都是通过 LayoutInflater 来 inflate 成具体的 View 对象的,刚接触 Android 开发的同学可能会疑惑,我没有用过这个东西啊,它是什么鬼?那是因为我们在 Activity 中加载布局是通过 setContentView() 函数完成的

public void setContentView(int layoutResID) {
    // 这里调用 getWindow() 拿到 window 对象
    // 然后调用 mWindow.setContentView() 函数
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

Window.java 是一个抽象类,继承并实现它的是 PhoneWindow.java , mWindow 是在 Activity 的 attach() 函数中被初始化的,这个函数是在 ActivityThread 中在创建 Activity 时被调用,我们来看一下 attach() 的源码

// 一堆参数忽略不用管他
final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config,
            String referrer, IVoiceInteractor voiceInteractor) {
    // 设置 baseContext 
    attachBaseContext(context);
    // 重点在这里,new 了一个 PhoneWindow 对象
    mWindow = new PhoneWindow(this);
    // 这里先不说,后面会讲到
    mWindow.getLayoutInflater().setPrivateFactory(this);
    // 此后省略部分无关代码
}

接下来,我们再进一步深入到 PhoneWindow 中看看构造函数都做了什么

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}

PhoneWindow 的构造函数除了调用 super() 外就只是初始化了一个 LayoutInflater 对象,mWindow.getLayoutInflater() 返回的就是 mLayoutInflater 对象,那么 mLayoutInflater 是到底如何被创建的?我们继续跟代码

public static LayoutInflater from(Context context) {
    LayoutInflater LayoutInflater =
            (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    if (LayoutInflater == null) {
        throw new AssertionError("LayoutInflater not found.");
    }
    return LayoutInflater;
}

这里的 context 是 Activity 对象,以下是 Activity 的 getSystemService() 函数代码

@Override
public Object getSystemService(@ServiceName @NonNull String name) {
    if (getBaseContext() == null) {
        throw new IllegalStateException(
                "System services not available to Activities before onCreate()");
    }

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

最终调用了 super.getSystemService(name)

@Override 
public Object getSystemService(String name) {
    if (LAYOUT_INFLATER_SERVICE.equals(name)) {
        if (mInflater == null) {
            // 实例化 mInflater
            mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
        }
        return mInflater;
    }
    return getBaseContext().getSystemService(name);
}

这里是调用了 baseContext 的 getSystemService() 函数,获取一个实例,然后调用 cloneInContext() 复制一个新的实例
getBaseContext() 是 ContextWrapper 的函数, Activity 继承 ContextThemeWrapper, ContextThemeWrapper 继承了 ContextWrapper,它是在 Activity 的 attach() 函数中通过 attachBaseContext() 进行的赋值,那我们就有必要知道这里的 baseContext 到底是什么,getSystemService() 做了些什么
我们在 ActivityThread 中找到对应的代码如下

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    Activity activity = null;
    try { // 通过反射 new 一个 Activity 对象
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        // ...
    } catch (Exception e) {
        // ...
    }
    Context appContext = createBaseContextForActivity(r, activity);
    // ...
    activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor);
    // ... 各种状态设置以及异常处理
    return activity;
}
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
    // ...
    // 通过 ContextImpl 为 Activity 创建一个 Context 对象
    ContextImpl appContext = ContextImpl.createActivityContext(
            this, r.packageInfo, displayId, r.overrideConfig);
    appContext.setOuterContext(activity);
    Context baseContext = appContext;
    // ...
    return baseContext;
}

距离真相又近了一步

static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    // 直接返回新的 ContextImpl 对象
    return new ContextImpl(null, mainThread, packageInfo, null, null, false,
            null, overrideConfiguration, displayId);
}

ContextImpl.createActivityContext() 只是 new 了一个 ContextImpl 对象,构造函数我们不再分析,直接看 ContextImpl 的 getSystemService() 函数

@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}

接着看 SystemServiceRegistry 的部分代码

static {
    // ... 省略大量代码
    registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
            new CachedServiceFetcher<LayoutInflater>() {
        @Override
        public LayoutInflater createService(ContextImpl ctx) {
            return new PhoneLayoutInflater(ctx.getOuterContext());
        }});
    // ... 省略大量代码
}
/**
 * Gets a system service from a given context.
 */
public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
}

我们最终的结论是在 Activity 中通过 getSystemService(LAYOUT_INFLATER_SERVICE) 得到的是一个 PhoneLayoutInflater 对象,他的代码十分简单

public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

    public PhoneLayoutInflater(Context context) {
        super(context);
    }

    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }

    /** Override onCreateView to instantiate names that correspond to the
        widgets known to the Widget factory. If we don't find a match,
        call through to our super class.
    */
    @Override 
    protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }

    public LayoutInflater cloneInContext(Context newContext) {
        return new PhoneLayoutInflater(this, newContext);
    }
}

PhoneLayoutInflater 是用来解析系统控件的,比如 TextView 、Button 、ImageView 等,通过 name 与 prefix 拼接得到类全名,如果解析失败,就调用 super
前面扯了一大堆没用的,我们回过头来看 mWindow.setContentView() 里面做了什么事

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
       final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
               getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
}

在 Activity 第一次被创建的时候 mContentParent 一定是空的,所以走 installDecor() 函数,我们简单的认为此 Activity 没有 transition 动画,所以会调用 mLayoutInflater.inflate(layoutResID, mContentParent) 去加载布局文件
在分析 LayoutInflater.inflate() 函数之前,我们出于好奇,看一下 installDecor() 是干什么的

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();
        // 省略部分代码
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        // 省略代码...
    }
}
protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}
// DecorView 是继承自 FrameLayout 的一个内部 View
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
    public DecorView(Context context, int featureId) {
        super(context);
        // ...
    }
    // ...
}
protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.
    TypedArray a = getWindowStyle();
    // ... 根据主题设置各种属性
    a.recycle();
    int layoutResource;
    // 这里主要是根据 theme 不同选择不同的容器布局
    if (xxx) {
        layoutResource = R.layout.xxx;
    } else { // 不止两个条件,此处简写
        layoutResource = R.layout.xxx;
    }
    // 实例化容器布局
    View in = mLayoutInflater.inflate(layoutResource, null);
    // 添加到 decor 中,并且是充满 decor
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    // ID_ANDROID_CONTENT 的值为 com.android.internal.R.id.content
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn't find content container view");
    }
    // ..
    return contentParent;
}

通过上面的代码我们可以看到,在 Activity 中通过 setContentView() 设置的布局,实际上最后是被添加到了 mContentParent 中,通过查看源码发现,mContentParent 其实也是一个 FrameLayout
别的不多扯,下面进入正题,分析 PhoneLayoutInflater.java 前面已经贴过了它的源码,它的构造函数只是调用了 super

protected LayoutInflater(Context context) {
    mContext = context;
}
protected LayoutInflater(LayoutInflater original, Context newContext) {
    mContext = newContext;
    mFactory = original.mFactory;
    mFactory2 = original.mFactory2;
    mPrivateFactory = original.mPrivateFactory;
    setFilter(original.mFilter);
}

也很简单,一个参数的就是存储一下 context 对象,两个参数的,还把 original 的三个回调以及过滤器赋值给自己
Filter

public interface Filter {
    // 作用是是否允实例化 clazz
    boolean onLoadClass(Class clazz);
}

Factory

public interface Factory {
    // 用来解析 xml 中自定义的 tag ,建议自定义 tag 加入自己的包名以区别于系统的
    public View onCreateView(String name, Context context, AttributeSet attrs);
}

Factory2

public interface Factory2 extends Factory {
    // 多了一个父布局 parent
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

和 setFactory 相关的函数一共有三个

public void setFactory(LayoutInflater.Factory factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

public void setFactory2(Factory2 factory) {
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}
/**
 * @hide for use by framework
 */
public void setPrivateFactory(Factory2 factory) {
    if (mPrivateFactory == null) {
        mPrivateFactory = factory;
    } else {
        mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
    }
}

通过上面的代码,我们可以知道,Factory 和 Factory2 两个只能设置其中一个,setPrivateFactory() 是隐藏 api ,setFactory() 或者 setFactory2() 函数要求在调用之前,最好用 cloneInContext() 获取一个新的实例,而且原来的 Factory 依然存在,会和自己的 Factory 合并
FactoryMerger

private static class FactoryMerger implements Factory2 {
    private final Factory mF1, mF2;
    private final Factory2 mF12, mF22;

    FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
        mF1 = f1;
        mF2 = f2;
        mF12 = f12;
        mF22 = f22;
    }

    public View onCreateView(String name, Context context, AttributeSet attrs) {
        View v = mF1.onCreateView(name, context, attrs);
        if (v != null) return v;
        return mF2.onCreateView(name, context, attrs);
    }

    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
                : mF1.onCreateView(name, context, attrs);
        if (v != null) return v;
        return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
                : mF2.onCreateView(name, context, attrs);
    }
}

FactoryMerger 实现了 Factory2 接口,构造函数有四个参数,前两个是新的 Factory ,后两个参数是旧的 Factory ,回调中先调用新的 Factory ,如果返回为空,则调用旧的 Factory
主角 inflate() 函数登场,相关函数一共四个,但最终都是调用的同一个

// 解析 xml 资源 如果 root 不为空,resource 生成的 view 会被添加到 root 中
public View inflate(int resource, ViewGroup root) {
    return inflate(resource, root, root != null);
}
// 解析一个 xml 文件
public View inflate(XmlPullParser parser, ViewGroup root) {
    return inflate(parser, root, root != null);
}
// 解析 xml 资源 第三个参数用来指定 view 是否应该 add 到 root 里
// 如果 root 为空 则地三个参数没有意义
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    // 通过 Resources 获取一个 XmlResourceParser
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}
// 上面三个函数最终都会调用这个函数,他是重点
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    // 重点,下面单独贴
}

从上面的代码,我们知道,inflate() 函数有四个重载函数,最后都只会调用一个函数,下面是代码

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        final Context inflaterContext = mContext;
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context) mConstructorArgs[0];
        // mConstructorArgs 是长度为 2 的 Object 数组
        // 第 0 个是 context 对象 第 1 个 是 AttributeSet 对象
        mConstructorArgs[0] = inflaterContext;
        View result = root;
        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG &&
                type != XmlPullParser.END_DOCUMENT) {
        }
        if (type != XmlPullParser.START_TAG) {
            throw new InflateException(parser.getPositionDescription()
                    + ": No start tag found!");
        }
        final String name = parser.getName();
        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");
            }
            rInflate(parser, root, inflaterContext, attrs, false);
        } else {
            // 创建 view
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            ViewGroup.LayoutParams params = null;
            if (root != null) {
                // 生成 LayoutParams
                params = root.generateLayoutParams(attrs);
                if (!attachToRoot) {
                    temp.setLayoutParams(params);
                }
            }
            // 解析 temp 的子 view
            rInflateChildren(parser, temp, attrs, true);
            if (root != null && attachToRoot) {
                // 如果 root 不为空并且 attachToRoot 为 true 
                // 则把 xml 解析完成的 view 添加到 root 中
                root.addView(temp, params);
            }
            if (root == null || !attachToRoot) {
                result = temp;
            }
        }
        // 如果 view 被添加到了 root 则返回 root
        // 否则返回 xml 解析出来的 view
        return result;
    }
}

其实 LayoutInflater 是使用的 pull 解析方式来解析的 xml ,关于 xml 解析方式,可以去网上搜,不止 pull 一种解析方式
可以看出,如果 xml 的根标签是 merge 那么 root 不能为空并且 attachToRoot 必须为 true ,否则会抛异常,后面调用了 createViewFromTag() ,就是根据 tag 节点创建 View

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                       boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        // 如果是 view 标签就获取 class 的值
        name = attrs.getAttributeValue(null, "class");
    }
    if (!ignoreThemeAttr) {
        // 如果设置了主题
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            // 则生成一个带有主题的 context
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }
    if (name.equals(TAG_1995)) {
        // 被 blink 包住的 view 会一闪一闪的,可以看 BlinkLayout 源码
        return new BlinkLayout(context, attrs);
    }
    View view;
    if (mFactory2 != null) {
        // 如果 mFactory2 不为空则调用 mFactory2.onCreateView()
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    // 如果经过上面的步骤 view 还是空,并且 mPrivateFactory 不为空
    // 则回调 mPrivateFactory.onCreateView()
    // 我们前面分析了,Activity 的 LayoutInflater 是设置了 PrivateFactory 的
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    if (view == null) {
        final Object lastContext = mConstructorArgs[0];
        mConstructorArgs[0] = context;
        try {
            if (-1 == name.indexOf('.')) { // 如果 tag 中没有 . 
                // 如果子类没有重写 onCreateView() 则默认拼接 android.view. 作为类名
                // PhoneLayoutInflater 重写了 onCreateView() 上面提到过
                view = onCreateView(parent, name, attrs);
            } else {
                view = createView(name, null, attrs);
            }
        } finally {
            mConstructorArgs[0] = lastContext;
        }
    }
    return view;
}

createViewFromTag() 中调用了 createView() 函数,第二个参数代表要拼接到 name 前的前缀

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;
    if (constructor == null) {
        // 使用 ClassLoader 加载类
        clazz = mContext.getClassLoader().loadClass(
                prefix != null ? (prefix + name) : name).asSubclass(View.class);
        if (mFilter != null && clazz != null) {
            boolean allowed = mFilter.onLoadClass(clazz);
            if (!allowed) { // 如果不允许加载,就抛异常
                failNotAllowed(name, prefix, attrs);
            }
        }
        // 拿到两参的构造器,存起来
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        if (mFilter != null) {
            // 获取之前保存过的状态
            Boolean allowedState = mFilterMap.get(name);
            if (allowedState == null) {
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                // 把是否允许状态保存起来
                mFilterMap.put(name, allowed);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            } else if (allowedState.equals(Boolean.FALSE)) {
                failNotAllowed(name, prefix, attrs);
            }
        }
    }
    Object[] args = mConstructorArgs;
    args[1] = attrs;
    // 实例化 view
    final View view = constructor.newInstance(args);
    if (view instanceof ViewStub) { // ViewStub 懒加载
        final ViewStub viewStub = (ViewStub) view;
        viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
    }
    return view;
}

通过上面的步骤,只是解析完成了 xml 的根 view ,接下来要循环解析 xml 中的 子 view

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
                            boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
              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_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }
    if (finishInflate) {
        parent.onFinishInflate();
    }
}

上面的代码就是根据不同的 tag 做不同的解析,如果不是 requestFocus 、tag 、include 或者 merge 就调用 createViewFromTag() 然后循环 rInflateChildren(),直到解析出所有 View ,限于篇幅原因,LayoutInflater 对各种 tag 的解析,留做后续讲解

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

推荐阅读更多精彩内容