View 的創建 - LayoutInflater 基礎流程分析

LayoutInflater 將布局文件(XML)實例化為一個 View 對象。

通常我們會通過 Activity#getLayoutInflater() 或者是 context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) 來獲取一個標準的與當前運行的 Context 相關聯的 LayoutInflater 實例。

我們以 Activity#setContentView(@LayoutRes int layoutResID) 為例來看一下 LayoutInflater 的工作流程。

源碼:Android-29、AndroidX

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

1 AppCompatActivity#setContentView 流程

AppCompatActivity 是 AndroidX 兼容包下的 Activity 的實現基類

//#AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}

可以看到內部通過代理來進行設置,下面來看代理的實現 getDelegate()

////#AppCompatActivity
@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

@NonNull
public static AppCompatDelegate create(@NonNull Activity activity,
        @Nullable AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, callback);
}

通過 getDelegate() 方法創建了代理的實現類 AppCompatDelegateImpl,下面我們來看這個代理實現類中的 setContentView 方法:

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}

可以看見,AppCompatActivity#setContentView 內部是通過 LayoutInflater.from(mContext).inflate(resId, contentParent) 來加載布局的

2 LayoutInflater.from(mContext) 流程

在來看 from(mContext) 方法之前,我們先明確 mContext 的類型。

2.1 mContext 的類型

往上翻可以看到我們在創建代理對象的同時,將 Activity 作為參數傳入,也就是說 mContext 的類型是 AppCompatActivity 也就是 ContextThemeWrapper 類型。

這里我們貼上一張 Context 相關的背景知識:


Context -android-29-

2.2 LayoutInflater.from(mContext)

/**
 * Obtains the LayoutInflater from the given context.
 */
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.getSystemService 來獲取 LayoutInflater “服務”。
之前我們已經明確了 mContext 的類型是 ContextThemeWrapper 類型,我們來看看它的 from 方法:

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

這個的 getBaseContext 獲取到的是 mBase 這個屬性,它的類型是 ContextImpl,mBase 的賦值源于 Activity 在創建后的調用的 attach 方法,這里就不再展開了。

然后讓我們來看看 ContextImplgetSystemService 方法:

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

到這里看上去是真正要獲取服務的地方了,我們根據 ContextImpl 對象本身和服務名稱SystemServiceRegistry 中獲取服務,我們可以把它理解為注冊表:

public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
}

跟進到它的 getSystemService 方法中可以看到:

  1. 從 SYSTEM_SERVICE_FETCHERS 中獲取 ServiceFetcher
  2. 通過 ServiceFetcher 獲取服務

服務在什么時候注冊?
SystemServiceRegistry 的靜態代碼塊中,對服務進行了注冊:

registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() {
    @Override
    public LayoutInflater createService(ContextImpl ctx) {
        return new PhoneLayoutInflater(ctx.getOuterContext());
    }});

可以看到最終我們獲取到的 LayoutInflater 類型是 PhoneLayoutInflater 類型。

在 ContextImpl 類中有一個 mServiceCache 屬性,在聲明的時候已經初始化:

//ContextImpl
// The system service cache for the system services that are cached per-ContextImpl.
    @UnsupportedAppUsage
    final Object[] mServiceCache = SystemServiceRegistry.createServiceCache();

由此可見,在 ContextImpl 創建后,服務就會進行注冊,根據注釋可知,每一個 ContextImpl 對象都有自己的服務緩存。

ServiceFetcher如何獲取服務?
我們在回過頭來看服務獲取的具體邏輯(我刪除了部分代碼):

public final T getService(ContextImpl ctx) {
    final Object[] cache = ctx.mServiceCache;
    final int[] gates = ctx.mServiceInitializationStateArray;
    for (;;) {
        boolean doInitialize = false;
        synchronized (cache) {
            // Return it if we already have a cached instance.
            //①
            T service = (T) cache[mCacheIndex];
            if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {
                return service;
            }
         if (doInitialize) {
            // Only the first thread gets here.
            T service = null;
            try {
            //②
                service = createService(ctx);
                newState = ContextImpl.STATE_READY;
            } catch (ServiceNotFoundException e) {
                onServiceNotFound(e);
            } finally {
                synchronized (cache) {
                    ③
                    cache[mCacheIndex] = service;
                    gates[mCacheIndex] = newState;
                    cache.notifyAll();
                }
            }
            return service;
        }
    }
}
  1. 查看 ContextImpl 的緩存,緩存命中則直接返回服務
  2. 緩存不存在的話則通過 createService 方法創建服務
  3. 最后將服務緩存在 ContextImpl 中

2.3 PhoneLayoutInflater#cloneInContext

在上面 ContextThemeWrapper 獲取服務的代碼中 mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); 我們在獲取 PhoneLayoutInflater 之后,還調用了 cloneInContext 方法,名字聽上去是克隆一個對象。

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

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

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

由此可見我們使用了一個新的 Context 替換了原先 LayoutInflater 中的 mContext 屬性,根據上面的代碼可知,我們在使用 ContextImpl 創建了 PhoneLayoutInflater 之后,將其中的 mContext 替換為 ContextThemeWrapper。

3 LayoutInflater#inflate

LayoutInflater.from(mContext).inflate(resId, contentParent); 由上面分析可知,在獲取到 PhoneLayoutInflater 對象后,接著調用它的 inflate 方法:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
              + Integer.toHexString(resource) + ")");
    }
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

最終調用的 inflate 方法接收三個參數:

  1. @LayoutRes int resource 這個參數就是我們 setContentView 方法傳入的 XML 資源文件
  2. @Nullable ViewGroup root 這是一個可選的父布局容器
  3. boolean attachToRoot ,這個參數決定是否將從 XML 加載的 View 對象添加進 root 中

根據 setContentView 的調用可知,我們傳入的布局文件最后會被添加進 id 為 Content 的容器中

在 inflate 方法中我們創建了當前 XML 資源文件的解析器,并將它傳入重載的 inflate 房中:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            View result = root;
            try {
                //① merge 標簽判斷
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        ...
                    }
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // ②創建 XMl 文件中頂層標簽中標記的 View 對象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                    // ③指定 LayoutParams
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    //④遞歸的將子視圖填充到 temp 中
                    context.rInflateChildren(parser, temp, attrs, true);

                    //⑤將根據 XML 實例化的 View 添加到 root 中                    
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    //直接返回 temp 對象
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                ....
            }

            return result;
        }
    }

這里的方法有點長,我同樣省略一些代碼,讓我們的視線集中在布局的解析上。

3.1 merge 標簽

我們判斷 XMl 的根標簽是不是 merge,如果是 merge 標記的話在直接調用遞歸填充方法 rInflater,這里我們先不看對 merge 標簽的處理。

3.2 LayoutInflater#createViewFromTag

如果不是 merge 標簽的話,我們將調用 createViewFromTag 方法來創建 View 對象:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        //...
        try {
            View view = tryCreateView(parent, name, context, attrs);

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(context, parent, name, attrs);
                    } else {
                        view = createView(context, name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            //...
        }
    }

這里也是分為三步:

  1. 嘗試調用 tryCreateView 方法創建 View
  2. 如果不是自定義 View 則調用 onCreateView 方法
  3. 如果是自定義 View 則調用 createView

3.2.1 LayoutInflater#tryCreateView

public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }
    View view;
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    return view;
}
  1. 如果 XML 的標記是 blink(TAG_1995) 的話,會創建一個 BlinkLayout 類型的對象,它會每隔 500ms 重繪一次,形成閃爍的效果。
  2. 嘗試通過 mFactory2 對象的 onCreateView 來創建 View 對象
  3. 嘗試通過 mFactory 對象的 onCreateView 來創建 View 對象
  4. 嘗試通過 mPrivateFactory 對象的 onCreateView 來創建 View 對象

mFactory2、mPrivateFactory 對象都是 Factory2 類型,而 mFactory 對象是 Factory 類型,Factory2 是對 Factory 接口的升級,Factory2 繼承了 Factory 接口,并且多了一個 View onCreateView(View parent, String name,Context context, @NonNull AttributeSet attrs); 相比較于 Factory 的 onCreateView 而言多了一個 View 類型的 parent 參數。

稍后我們再來看這幾個屬性是在何時被設置的,讓我們回到 createViewFromTag 方法

3.2.2 LayoutInflater#createView

如果我們沒有設置 mFactory2 這些屬性,那么 tryOnCreateView 這個方法返回的 View 對象就是 null,之后我們會根據 XML 標記是否包含 "." 來決定調用不同的方法。

如果標簽不包含 "." ,說明這是 Android 系統提供的 View 例如:TextView,ImageView...,這時我們會調用 onCreateView 方法,這個方法會調用一些列的重載方法,最后會調用 createView(String name, String prefix, AttributeSet attrs) 方法:

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    Context context = (Context) mConstructorArgs[0];
    if (context == null) {
        context = mContext;
    }
    return createView(context, name, prefix, attrs);
}

在 onCreateView 方法當中,我們會調用 createView 方法,并且限定了 prefix 這個入參為 "android.view",緊接著我們會調用最后重載的 createView 方法:

public final View createView(@NonNull Context viewContext, @NonNull String name,
            @Nullable String prefix, @Nullable AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                        mContext.getClassLoader()).asSubclass(View.class);
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                ...
            }
        
            try {
                final View view = constructor.newInstance(args);
                if (view instanceof ViewStub) {
                    // Use the same context when inflating ViewStub later.
                    final ViewStub viewStub = (ViewStub) view;
                    viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
                }
                return view;
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        } 
        ...
    }

我省略一些異常捕獲和日志代碼,可以看到我們會嘗試從緩存中獲取當前 View 的構造函數,如果命中緩存的話,則直接通過反射創建 View 對象并返回,否則我們會通過入參 nameprefix 來拼接 View 的全路徑類名,然后在通過反射獲取它的構造函數,放入緩存之后在創建 View 返回。

到這里我們可以說 LayoutInflater 是通過反射的方式將 XML 實例化成一個 View 對象,這么說沒錯,但是之前我們有提到過,在反射創建 View 對象之前,會經過 mFactory 等對象的處理,接下來讓我們肯看這些“工廠“是什么時候設置的。

4 LayoutInflater#setFactory2

之前我們分析的是 AppCompatActivity#setContentView 流程:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

在 setContentView 之前我們調用了父類的 onCreate 方法:

 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
     final AppCompatDelegate delegate = getDelegate();
     delegate.installViewFactory();
     delegate.onCreate(savedInstanceState);
     super.onCreate(savedInstanceState);
 }

這里可以看到我們調用了 delegate 對象的 installViewFactory 方法:

//AppCompatDelegateImpl
@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

其中 LayoutInflaterCompat.setFactory2(layoutInflater, this);對 mFactory 進行了設置:

//LayoutInflaterCompat
class AppCompatDelegateImpl extends AppCompatDelegate
        implements MenuBuilder.Callback, LayoutInflater.Factory2{
        //......
}

public static void setFactory2(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
    inflater.setFactory2(factory);
    //省略了兼容性代碼......
}

AppCompatDelegateImpl 實現了 Factory2 接口,我們通過 setFactory2 方法將 this 賦值給 mFactory 和 mFactory2 對象,下面我們來看 Factory2 接口在 AppCompatDelegateImpl 中的實現:

 @Override
 public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
     return createView(parent, name, context, attrs);
 }
 
 public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        //省略部分代碼......
        mAppCompatViewInflater = new AppCompatViewInflater();
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed()
        );
    }

可以看到在 createView 方法中,我們構建了一個 AppCompatViewInflater 對象,將創建 View 的操作交給了它,接著我們看這個對象的 createView 方法:

final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;
    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        // We then apply the theme on the context, if specified
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }
    View view = null;
    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
    // 省略了后續代碼...
}

protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}
  1. 可以看見,為了能夠兼容,使得 android 5.0 之前的版本能夠使用矢量圖功能,我們會對originalContext 進行包裝,通過 TintContextWrapper 的 wrapper 方法將它包裝成 TintContextWrapper 類型
  2. 通過字符串匹配,直接使用 new 對象的方式,創建相對應的 View,節省了反射帶來的開銷

由此可見,我們雖然我們在 XML 中聲明的是 TextView 類型,但是為了向前兼容,系統實際創建的是 AppCompatTextView 類型;另外通過 View#getContext 方法獲取到的類型也不一定是 Activity 類型,也有可能是 TintContextWrapper 類型。

5 LayoutInflater#setPrivateFactory

經過上面的流程分析,我們已經知道 mFactory 和 mFactory2 屬性是什么時候被賦值的,那么 mPrivateFactory 又是什么時候被賦值的呢?

/**
 * @hide for use by framework
 */
@UnsupportedAppUsage
public void setPrivateFactory(Factory2 factory) {
    if (mPrivateFactory == null) {
        mPrivateFactory = factory;
    } else {
        mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
    }
}

我們可以通過 LayoutInflater#setPrivateFactory 方法設置 mPrivateFactory,注釋中說這是個隱藏方法,被 framework 層使用,這里給大家一個看源碼的網站,就是谷歌最近剛開源的 Code Serach,我們通過這個網站來查看一下
setPrivateFactory 方法的引用。

-w1434

如圖,我們在 Activity attach 方法調用的時候通過 mWindow.getLayoutInflater().setPrivateFactory(this);對 mPrivateFactory 進行設置,并且設置的值是 this,說明我們 Activity 也實現了 Factory2 接口。

下面我們來看 Factory2 接口在 Activity 中的實現:

public View onCreateView(@Nullable View parent, @NonNull String name,
        @NonNull Context context, @NonNull AttributeSet attrs) {
    if (!"fragment".equals(name)) {
        return onCreateView(name, context, attrs);
    }
    return mFragments.onCreateView(parent, name, context, attrs);
}

可以看到這里對 XML 中的 fragment 標記進行了處理,FragmentController#onCreateView 方法會對 Fragment 進行實例化,并調用 Fragment 實現的 onCreateView 方法來返回 View 對象。

到此為止我們就知道了 XML 中的 fragment 標簽是如何被處理的。

6. final

Android 系統通過給 LayoutInflater 設置工廠的方式,自己決定 View 的實例化,以此來實現向前兼容,利用 setFactory 方法我們還可以做到很多事情,比如全局字體的替換,給特定的 View 設置特定的背景...

同時我也只對最基礎的流程進行分析,里面對 merge、include、viewStub 等標簽的處理并沒有展開,其實這些標簽也只是遞歸的進行 View 的創建并添加進容器而已。

參考鏈接:

  1. https://juejin.im/post/5dd499a6f265da0bf21126cc
  2. https://blog.csdn.net/mq2553299/article/details/99737681
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念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

推薦閱讀更多精彩內容