Layout inflate方法解析:由Xml文件生成View Hierarchy的一些細節

太長了,不想看?

LayoutInflater.inflate()方法能將Xml格式的布局文件轉換成以父子關系組合的一系列View,轉換后的結構也稱為View Hierarchy。

我們通常向inflate()方法傳入三個參數:布局資源的resId,可為空的ViewGroup:root,以及布爾值attachToRoot。許多初學者對于后兩個參數的使用比較生硬,缺乏理解。

我理解的引入root的原因是為了讀取Xml文件中最外層標簽的布局屬性。布局屬性就是我們常看到的以<layout->開頭的屬性,他表示一個View希望在父布局中獲得的尺寸以及位置,因此布局屬性是提供給父布局讀取以便計算尺寸、位置的。布局屬性體現在View中就是mLayoutParams變量,這個變量的類型是ViewGroup.LayoutParams。不同的ViewGroup的子類實現了不同的LayoutParams類,用以從Xml中讀取自己關心的布局屬性,一個View的mLayoutParams的類型必須與其父布局實現的LayoutParams相一致。

對于Xml文件的最外層標簽,他所轉換成的View并不知道自己的父布局會是什么類型的,因此他不會生成mLayoutParams變量,此時該標簽的所有<layout-XXX>屬性都沒有應用到View中。為了避免這種情況,我們需要告知最外層的View他的父布局是什么類型的,生成對應的LayoutParams儲存布局屬性。root就能幫助我們生成LayoutParams,而如果root正是我們希望的父布局,那么我們就將attachToRoot設為true,這樣我們通過Xml生成的View Hierarchy可以直接加入到root中,我們不需要手動做這步操作了。

如果我們將Xml文件的嵌套結構看作是樹狀結構的話,逐個標簽的解析其實就是樹的深度優先遍歷,我們在遍歷的同時生成了一棵以View為節點,使用父子關系關聯的樹。

View Hierarchy中每個View的生成有四步:

  1. 由標簽生成一個View
  2. 根據View的父布局的類型生成對應的LayoutParams,并將LayoutParams設置給View
  3. 生成View的所有Children
  4. 將View加入他的父布局中

由一個標簽轉換成一個View的過程其實就是通過ClassLoader加載出標簽對應的View.Class文件并獲得構造器,相當于調用View(Context context, @Nullable AttributeSet attrs)構造一個實例。因此我們的自定義控件需要實現該構造方法才能在Xml中使用,隨后從attrs變量中獲得Xml中的屬性。

inflate方法介紹

Android開發中,我們使用LayoutInflater.inflate()方法將layout目錄下Xml格式的資源文件轉換為一個View Hierarchy,并返回一個View對象。

View Hierarchy直譯大概就是視圖層次,指的是以父子關系關聯的一系列View,我們通過View Hierarchy的根節點(root view)可以獲得該結構中所有的View對象。

如果讓我們自己去設計一個方法將布局文件轉換為View Hierarchy,我們可能會想到逐行讀取Xml中的標簽,利用標簽的信息生成一個新的View/ViewGroup,當Xml中出現嵌套關系就意味我們需要使用父子關系關聯兩個View。而inflate()方法做的就是這么一件事。

我們可以使用Activity.getLayoutInflater()等方法獲得一個LayoutInflater的實例,這些方法本質上都是通過getSystemService(Context.LAYOUT_INFLATER_SERVICE)獲得一個系統服務,這個方法最終會返回一個PhoneLayoutInflater的實例。

inflate有幾種重載方法,但最終都會走到

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

第一個參數我們看著比較陌生,但是大部分情況下我們只需要傳入一個layout資源文件,Framework會幫我們根據資源獲得一個Parser。需要注意的是為了提升使用時的效率,Android會在編譯時就去解析layout資源文件并生成Parser,因此我們想通過inflate()方法在使用過程中使用一個單純的Xml文件(非布局資源)去生成View是不可行的。

后面兩個參數會在源碼解析過程中介紹。

我們看下官方對這幾個參數的定義:

參數 意義
parser XmlPullParser:以Pull的方式解析Xml文件,通過Parser對象方便我們操作
root ViewGroup:可選項。當attachToRoot為true時,他將作為生成出來的View層級的parent。如果attachToRoot為false,那root僅僅為View層級樹的根節點提供LayoutParams的值
attachToRoot 配合root使用

inflate()方法的返回值是一個View,如果root不會為空且attachToRoot為true,返回root。否則返回Xml生成的View Hierarchy的根View。

Inflate 方法源碼解析

看下inflate階段的核心代碼

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    // 解析根節點
    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 {
        // 將Xml最外層的標簽解析為View對象,記為temp
        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
        
        if (root != null) {
            // 當root不為空時,創建與root相匹配的LayoutParams
            params = root.generateLayoutParams(attrs);
            if (!attachToRoot) {
                // 僅將params提供給temp
                temp.setLayoutParams(params);
            }
        }
        
        // 通過inflate生成temp的所有chiildre
        rInflateChildren(parser, temp, attrs, true);
        
        // root不為空且attachToRoot為true,將temp加入到root中,且用到params
        if (root != null && attachToRoot) {
            root.addView(temp, params);
        }
        
        if (root != null && attachToRoot) 
            return root;
        }else {
            return temp;
        }
    }
}

先不考慮<merge>標簽,inflate()方法執行的流程:

  1. 將最外層的標簽轉換為一個View,記為temp。
  2. 當root不為空時,利用root生成的temp的LayoutParams。
  3. 解析Xml并生成temp的所有子View。
  4. 當root不為空且attachToRoot為true時,將temp添加為root的一個child。
  5. 當root不為空且attachToRoot為true時,返回root,否則返回temp。

這個流程包含了幾個細節:

  1. 由Xml標簽生成View對象
  2. 根據Xml嵌套結構生成View父子結構
  3. 應用root及attachToRoot

下面,我們由表及里的解析這幾個細節。首先來看一下傳參中root與attachToRoot兩個參數的作用。

root與attachToRoot

root在inflate()方法中的第一個作用就是生成一個LayoutParams。

if (root != null) {
    // 當root不為空時,創建與root相匹配的LayoutParams
    params = root.generateLayoutParams(attrs);
}

LayoutParams保存的是一個控件的布局屬性。那我們來看下為什么需要利用root生成LayoutParams。

布局屬性與LayoutParams

在Xml文件中,有許多前綴為layout_的屬性,比如我們最熟悉的layout_width/layout_height,我們稱其為布局屬性。View使用布局屬性來告知父布局它所希望的尺寸與位置。不同類型的父布局讀取的布局屬性不同,比如layout_centerInParent屬性,父布局為RelativeLayout時會起作用,而父布局為LinearLayout時則無法使用。

Xml中的布局屬性保存到View中就是mLayoutParams變量,它的類型是ViewGroup.LayoutParams。實際上ViewGroup的子類都會實現一個擴展自ViewGroup.LayoutParams的嵌套類,這個LayoutParams類決定了他會讀取哪些布局屬性。我們看下RelativeLayout.LayoutParams源碼的一部分:

public LayoutParams(Context c, AttributeSet attrs) {
    super(c, attrs);

    TypedArray a = c.obtainStyledAttributes(attrs,
            com.android.internal.R.styleable.RelativeLayout_Layout);
    ...
    
    final int N = a.getIndexCount();
    for (int i = 0; i < N; i++) {
        int attr = a.getIndex(i);
        switch (attr) {
            ...
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
                rules[LEFT_OF] = a.getResourceId(attr, 0);
                break;
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
                rules[RIGHT_OF] = a.getResourceId(attr, 0);
                break;
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
                rules[ABOVE] = a.getResourceId(attr, 0);
                break;
            case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
                rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
                break;
            ...
        }
    }
    ...
    a.recycle();
}

這段代碼跟我們自定義控件時讀取Xml中的自定義屬性是一樣的做法,我們看到RelativeLayout.LayoutParams在創建時讀取了一系列布局屬性并存儲,比如layout_centerInParent,其他的LayoutParams不會讀取該屬性。

如果對AttributeSet、TypedArray不熟悉可以參考這里:https://blog.csdn.net/lmj623565791/article/details/45022631

我們能在RelativeLayout.onMeasure()方法中找到對LayoutParams的使用。

// RelativeLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    int count = views.length;
    for (int i = 0; i < count; i++) {
        View child = views[i];
        if (child.getVisibility() != GONE) {
            ...
            LayoutParams params = (LayoutParams) child.getLayoutParams();
            int[] rules = params.getRules(layoutDirection);
            ...
        }
    }
    ...
}

當計算每個子View的位置時,需要讀取他們的布局屬性,此時會將View的LayoutParams強制類型轉換為RelativeLayout.LayoutParams,這里可能會報出無法轉換類型的錯誤,所以我們需要保證加入到RelativeLayout的View的LayoutParams類型都是RelativeLayout.LayoutParams

我們總結一下LayoutParams的核心知識點:

  1. LayoutParams保存了Xml中的layout_開頭的布局屬性
  2. ViewGroup子類通常會實現一個LayoutParams類,用于讀取他們需要的布局屬性
  3. View的LayoutParams類型必須與其父布局的類型相匹配,否則會在onMeasure過程中報錯

回到root與attachToRoot

當我們解析Xml中最外層的標簽,也就是View Hierarchy的根View時,程序并不知道它的父布局會是什么類型的,因此不會生成LayoutParams。這時最外層標簽中的所有布局屬性,包括layout_width/layout_height都不會被記錄到View對象中,也就是俗稱的“屬性失效了”。

但在實際使用中,我們通常能知道Xml生成的View Hierarchy所要加入的父布局或是要加入的父布局的類型。這時候我們傳入一個root參數,根據root的類型去讀取根View的布局屬性并生成對應的LayoutParams。這段代碼如下:

if (root != null) {
    // root不為空時,生成與root對應的LayoutParams
    params = root.generateLayoutParams(attrs);
}

public LayoutParams generateLayoutParams(AttributeSet attrs) {
    // 不同的ViewGroup生成LayoutParams的細節不同
    return new LayoutParams(getContext(), attrs);
}

attachToRoot則用于判斷root是直接作為parent使用還是僅需要他的類型信息。

if (root != null) {
    if (attachToRoot) {
        root.addView(temp, params);
    }else{
        // Set the layout params for temp if we are not attaching. 
        temp.setLayoutParams(params);
    }
}

遞歸處理Xml中的所有標簽

inflate方法中,除了根標簽以外所有剩余標簽的解析只使用了一個方法:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    // Inflate all children under temp against its context.
    rInflateChildren(parser, temp, attrs, true);            
    ...
}

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
        boolean finishInflate) throws XmlPullParserException, IOException {
    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

這個方法僅僅是調用rInflate方法,沒有其他額外的行為。rInflate方法的中r的含義是Recursive,即遞歸,我們可以猜測這個方法是使用遞歸的方式去處理Xml中的所有嵌套關系。我們看下rInflate核心部分:

這段方法涉及到XmlPullParser的知識,他將Xml文件轉換為一個對象。通過next()方法獲得下一個事件,一共有五個事件START_DOCUMENT、START_TAG、TEXT、END_TAG、END_DOCUMENT。并且通過depth取得當前元素嵌套的深度,未讀取到START_TAG時depth為0,每次讀取到START_TAG時depth加1。細節參考 http://www.xmlpull.org/

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) {
    
    final int depth = parser.getDepth();
    // while的結束條件:找到該depth的END_TAG(或者文件結束)
    while (((type = parser.next()) != XmlPullParser.END_TAG
        || parser.getDepth() > depth) 
        && type != XmlPullParser.END_DOCUMENT) {
            
        // 只有遇到START_TAG時進行解析
        if (type != XmlPullParser.START_TAG) {
            continue;
        }
    
        final String name = parser.getName();
    
        if (TAG_REQUEST_FOCUS.equals(name)) {
            ...
        } 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 {
            // 將當前Tag轉換為View
            final View view = createViewFromTag(parent, name, context, attrs);
            // 根據父布局類型讀取布局屬性
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 嵌套處理,解析當前View的所有children
            rInflateChildren(parser, view, attrs, true);
            // 將View加入到parent中
            viewGroup.addView(view, params);
        }
    }
}

我們配合例子分析下rInflate方法的流程:

                               <!-- depth -->
<LayoutA >                       <!-- 1 -->
    <ViewB />                    <!-- 2 -->
    <LayoutC >                   <!-- 2 -->
        <ViewC1 />               <!-- 3 -->
    </LayoutC >                  <!-- 2 -->
</LayoutA >                      <!-- 1 -->
1.首先記下當前的depth
2.取Xml中的下一個事件,直到到達文件的結尾,或者到達了當前depth的END_TAG

參照例子,如果rInflate開始時解析到了<LayoutC>,那么當解析到</LayoutC>時就會從while跳出,本次執行結束。

3. while循環中,遇到START_TAG時解析

只解析STAST_TAG是保證每個Tag都只生成一個View,比如在解析到<LayoutC>時生成一個View,解析到</LayoutC>時則不需要。

解析普通的TAG的邏輯如下:

// 將當前Tag轉換為View
final View view = createViewFromTag(parent, name, context, attrs);
// 根據父布局類型讀取布局屬性
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 嵌套處理,解析當前View的所有children
rInflateChildren(parser, view, attrs, true);
// 將View加入到parent中
viewGroup.addView(view, params);

這里需要關注的就是那個嵌套的rInflateChildren()方法,他也會走到rInflate,以當前解析出的View作為Parent,解析下一層的內容。

inflate過程推演

我們還是以上面的例子將inflate方法的執行流程推演一遍。

                               <!-- depth -->
<LayoutA >                       <!-- 1 -->
    <ViewB />                    <!-- 2 -->
    <LayoutC >                   <!-- 2 -->
        <ViewC />                <!-- 3 -->
    </LayoutC >                  <!-- 2 -->
</LayoutA >                      <!-- 1 -->
  • inflate方法,取到<LayoutA>,解析出LayoutA對象
  • 通過rInflate方法解析LayoutA的所有children,開始時depth為1
    • 取到<ViewB />,解析出ViewB對象,ViewB無children,它的rInflateChildren會讀到ViewB的END_TAG并結束,將ViewB加入到LayoutA中
    • 取到<LayoutC>,解析出LayoutC對象,調用rInflate方法解析Children
      • 取到<ViewC />,解析出ViewC對象,無Children,將ViewC加入到LayoutC中
      • 取到<LayoutC />,LayoutC的rInflateChildren過程結束
      • 將LayoutC對象加入到LayoutA中
    • 取到</LayoutA>,為END_TAG且depth為1,rInflate方法結束
  • View Hierarchy解析完成,配合root及attachToRoot做些處理便可返回

整個流程自上而下的解析了Xml文件中的所有Tag,并生成了對應的View Hierarchy,嵌套關系順利轉換成了父子關系。生成的View Hierarchy是一個樹狀結構,生成過程跟樹的深度優先遍歷有相似的感覺。

merge標簽解析

解析完rInflate方法后,我們再來看下<merge>標簽的解析:

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);
}

其實就是忽略<merge>這一層,將<merge>的所有內容都直接加入到root中。

由Tag生成對應的View

前面我們介紹了layout Xml如何轉換為View Hierarchy,這個過程中的最后一個細節就是單個Tag如何轉化成View對象

無論是inflate方法對根View的解析還是rInflate中的嵌套解析,都是調用createViewFromTag()方法,我們看下這個方法的核心部分。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    
    // <view>標簽中可以使用class來標注類        
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }
    
    // 除了<include>以外,ignoreThemeAttr總為ture,讀取Xml中的theme并通過Context作用到View上
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }

    // 就當是彩蛋吧,let's party
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }
    
    // 生成View
    try {
        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);
        }
    
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            // 用于構建View的參數
            // View(Context context, @Nullable AttributeSet attrs)
            // 第一個傳參是context
            attrs, int defStyleAttr)
            mConstructorArgs[0] = context;
            try {
                // 使用系統控件時我們可以不帶著命名空間,此時name中不包含"."
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
    
        return view;
    }
    ...
}    

直接看到生成View的部分

  1. 由Factory2生成
  2. 由Factory生成
  3. 由mPrivateFactory生成
  4. 由onCreateView/createView方法生成

這里的生成方法有先后順序,View一旦生成就不用走后面的方法。Factory2及Factory可以看做是給我們hook代碼的,允許我們按自己的期望去將Tag轉換為View,二者的區別是工廠方法的傳參不同。

如果沒有設置工廠

if (-1 == name.indexOf('.')) {
    view = onCreateView(parent, name, attrs);
} else {
    view = createView(name, null, attrs);
}

Xml中使用系統控件可以不加上命名空間,因此name中沒有“.”,在onCreateView方法中會為系統控件加上前綴“"android.view."”并調用createView方法。而我們前面提到了我們實際使用的LayoutInflater通常是PhoneLayoutInflater,他重寫了onCreateView方法:

/// PhoneLayoutInflater.java

@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);
}

private static final String[] sClassPrefixList = {
    "android.widget.",
    "android.webkit.",
    "android.app."
};

/// LayoutInflater.java

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

這里的操作都是為系統控件補全命名空間,具體的生成View的工作由createView完成,核心代碼如下:

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
    ...
    // 使用ClassLoader獲得構造器
    clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
    constructor = clazz.getConstructor(mConstructorSignature);
    constructor.setAccessible(true);  
    
    // View的構造器的傳參
    Object[] args = mConstructorArgs;
    args[1] = attrs;
    
    // 獲得View實例    
    final View view = constructor.newInstance(args);
    return view;
    ...
}

先通過ClassLoader加載類,獲得構造器,然后實例化View的子類。具體的構造方法是View(Context context, @Nullable AttributeSet attrs),因此我們的自定義控件也需要實現這個構造方法才能在Xml中正確使用。
`

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370