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閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容