Android View 繪制流程

Android 的繪制過程可分為3個步驟即:measure(測量)、layout(布局)、draw(繪制)。

一、 measure過程

1.1 MeasureSpec

MeasureSpec 是 View 測量過程中的一個關鍵參數,很大程度上決定了 View 的寬高,父容器會影響 View 的MeasureSpec 的創建,MeasureSpec 不是唯一由 LayoutParams 決定的,LayoutParams 需要和父容器一起才能決定 View 的MeasureSpec,從而進一步確定 View 的寬高,在 View 測量過程中,系統會將該 View 的 LayoutParams 參數在父容器的約束下轉換成對應的 MeasureSpec ,然后再根據這個 measureSpec 來測量 View 的寬高。

MeasureSpec 代表一個32位 int 值,高2位代表 SpecMode(測量模式),低30位代表 SpecSize(在某個測量模式下的規格大小),MeasureSpec 通過將 SpecMode 和 SpecSize 打包成一個 int 值來避免過多的內存分配,為了方便操作,其提供了打包和解包方法源碼如下:

private static final int MODE_SHIFT = 30;
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY     = 1 << MODE_SHIFT;
public static final int AT_MOST     = 2 << MODE_SHIFT;

//通過將 SpecMode 和 SpecSize 打包,獲取 MeasureSpec  
public static int makeMeasureSpec(int size, int mode) {
  if (sUseBrokenMakeMeasureSpec) {
    return size + mode;
  } else {
    return (size & ~MODE_MASK) | (mode & MODE_MASK);
  }
}

//將 MeasureSpec 解包獲取 SpecMode
public static int getMode(int measureSpec) {
      return (measureSpec & MODE_MASK);
}
//將 MeasureSpec 解包獲取 SpecSize
public static int getSize(int measureSpec) {
     return (measureSpec & ~MODE_MASK);
}

SpecMode 有三類,每一類都表示特殊的含義:

  1. UNSPECIFIED 父容器不對 View 有任何的限制,要多大給多大,這種情況下一般用于系統內部,表示一種測量的狀態。

  2. EXACTLY 父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值,它對應于LayoutParams 中的 match_parent 和具體的數值這兩種模式

  3. AT_MOST 父容器指定了一個可用大小即 SpecSize,View 的大小不能大于這個值,具體是什么值要看不同 View 的具體實現。它對應于 LayoutParams 中的 wrap_content。

    ?

1.2 測量過程

對于DecorView,它的 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 來決定;對于普通 View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定。

measure.png

對普通的 View 的 measure 方法的調用,是由其父容器傳遞而來的,這里先看一下 ViewGroup 的 measureChildWithMargins 方法:

 protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {

     //第一步,獲取子 View 的 LayoutParams  ,也就是我們在xml中設置的layout_width和layout_height。
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    //第二步,獲取子 view 的 WidthMeasureSpec,根據父View的測量規格和父View自己的Padding,
    //還有子View的Margin和已經用掉的空間大小(widthUsed),就能算出子View的MeasureSpec。
    //其中傳入的幾個參數說明:
    //parentWidthMeasureSpec 父容器的 WidthMeasureSpec
    //mPaddingLeft + mPaddingRight view 本身的 Padding 值,即內邊距值
    //lp.leftMargin + lp.rightMargin view 本身的 Margin 值,即外邊距值
    //widthUsed 父容器已經被占用空間值
    // lp.width view 本身期望的寬度 with 值

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);

                    
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    // 第三步,根據獲取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec 對子 view 進行測量
    //對子 view 進行測量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

看一下 getChildMeasureSpec 方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    // 獲取父容器的 specMode,父容器的測量模式影響子 View  的測量模式
    int specMode = MeasureSpec.getMode(spec);
    // 獲取父容器的 specSize 尺寸,這個尺寸是父容器用來約束子 View 大小的
    int specSize = MeasureSpec.getSize(spec);
    // 父容器尺寸減掉已經被用掉的尺寸,得到是子View的大小.
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
     //父View是EXACTLY的類型.  
    case MeasureSpec.EXACTLY:
        //如果子view是一個固定的值,即在xml中設置確定的值。
        if (childDimension >= 0) {
            //則大小為固定的值。
            resultSize = childDimension;
            //模式是EXACTLY
            resultMode = MeasureSpec.EXACTLY;
        } 
        //如果子View的類型是MATCH_PARENT。
        else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            //子view大小為父容器大小。
            resultSize = size;
            //模式是EXACTLY
            resultMode = MeasureSpec.EXACTLY;
        } 
        //如果子view的類型是WRAP_CONTENT。
        else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            //大小是最大值是size,具體由子view自己決定,最大不能超過size。
            resultSize = size;
            //模式是AT_MOST
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    // 父容器為 AT_MOST 最大測量模式
    case MeasureSpec.AT_MOST:
        //如果子view是一個固定的值,即在xml中設置確定的值。
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            //則大小為固定的值。
            resultSize = childDimension;
            //模式是EXACTLY
            resultMode = MeasureSpec.EXACTLY;
        } 
        //如果子View的類型是MATCH_PARENT。
        else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            //大小是最大值是size,具體由子view自己決定,最大不能超過size。
            resultSize = size;
            //模式是AT_MOST
            resultMode = MeasureSpec.AT_MOST;
        } 
        子View的width或height為 WRAP_CONTENT  
        else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            //同上
            resultSize = size;              
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    //父View是UNSPECIFIED的
    case MeasureSpec.UNSPECIFIED:
        //如果子view是一個固定的值,即在xml中設置確定的值。
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            //則大小為固定的值。
            resultSize = childDimension;
            //模式是EXACTLY
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            //子 View 尺寸為 0,測量模式為 UNSPECIFIED
            // 父容器不對 View 有任何的限制,要多大給多大
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            //子 View 尺寸為 0,測量模式為 UNSPECIFIED
            // 父容器不對 View 有任何的限制,要多大給多大
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

以上代碼有點多,但是邏輯也很清晰:

  • 如果父View傳進來的模式是EXACTLY類型,也就是父View的大小是確定的。
    1. 當當前的View是一個固定值,也就是我們在xml里面定義改View的width和height是固定的值的大小是的時候,它最后的大小就是View設置的值。模式是MeasureSpec.EXACTLY。
    2. 當當前View的大小是設置成match_parent的時候,則當前View 的大小是父VIew的大小size。模式是MeasureSpec.EXACTLY。
    3. 當當前的View設置成warp_content 內容包裹時,則View的大小由view自身決定,要多大就多大,但是不能超過父View的大小size,其實這時候是無法知道自己的大小,要等到child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 調用到時候才知道自身的大小。模式是MeasureSpec.AT_MOST。
  • 如果父View傳進來的模式是AT_MOST類型, 最大測量模式.
    1. 當當前的View是一個固定值,也就是我們在xml里面定義改View的width和height是固定的值的大小是的時候,它最后的大小就是View設置的值。模式是MeasureSpec.EXACTLY。
    2. 當當前View的大小是設置成match_parent的時候,則當前View 的大小是父VIew的大小size。但是父View也不知道自己的大小,所以模式是MeasureSpec.AT_MOST。
    3. 當當前的View設置成warp_content 內容包裹時,父View的大小是不確定(只知道最大只能多大),子View又是WRAP_CONTENT,那么在子View的Content沒算出大小之前,子View的大小最大就是父View的大小,所以子View MeasureSpec mode的就是AT_MOST,而size 暫定父View的 size。
  • 如果父View傳進來的模式是UNSPECIFIED類型
    1. 當當前的View是一個固定值,也就是我們在xml里面定義改View的width和height是固定的值的大小是的時候,它最后的大小就是View設置的值。模式是MeasureSpec.EXACTLY。
    2. 當當前View的大小是設置成match_parent的時候,子 View 尺寸為 0,測量模式為 UNSPECIFIED,父容器不對 View 有任何的限制,要多大給多大。
    3. 當當前的View設置成warp_content 內容包裹時,子 View 尺寸為 0,測量模式為 UNSPECIFIED,父容器不對 View 有任何的限制,要多大給多大。

1.3 View的measure過程

分兩種情況:

  1. 如果只是一個原始的 View,通過measure方法就完成了測量過程。
  2. 如果是一個 ViewGroup 除了完成自己的測量過程還會遍歷調用所有子 View 的measure方法,而且各個子 View 還會遞歸執行這個過程。

1.3.1 measure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

    。。。
    //調用onMeasure()開始繪制.
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    。。。

}

代碼有點長,主要看measure是一個final的方法,即不能被重寫,里面真正調用的是onMeasure()方法。所以我們在自定義View的時候可以重寫該方法。

1.3.2 onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //用于獲得View寬/高的測量值
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

onMeasure方法主要用來獲取View的寬和高的測量值。在往下看代碼前,先看一下默認值的獲取getDefaultSize().

1.3.3getDefaultSize

public static int getDefaultSize(int size, int measureSpec) {
    //默認大小.
    int result = size;
    //獲取測量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    //獲取大小
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

當傳進來的模式是UNSPECIFIED的時候,默認值是size,看一下size的獲取getSuggestedMinimumWidth().

1.3.4 getSuggestedMinimumWidth

protected int getSuggestedMinimumWidth() {
    //如果沒有設置背景,view的最小寬度是mMinWidth:
    // 1、mMinWidth = android:minWidth屬性所指定的值,2、若android:minWidth沒指定,則默認為0
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

如果沒有設置背景則返回的值是設置的android:minWidth屬性指定的值,若android:minWidth沒指定,則默認為0。如果有背景,則取兩者的最大值。

回過頭來再看,在onMeasure()里面調用setMeasuredDimension方法。

1.3.5 setMeasuredDimension

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

在這里又調用setMeasuredDimensionRaw 將測量后的View的寬高進行存儲。

1.3.6 setMeasuredDimensionRaw

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    //測量后的view寬高的值.
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

1.4 實際獲取View寬高

在實際開發中,我們在Actiivty中獲取View的寬高往往都是0,這是由于我們無法保證在Activity執行生命周期中,View已經測量完成,如果還沒測量完成,這時候去獲取,那結果肯定是0.以下幾種方法可以獲取View的寬高。

  1. Activity/View#onWindowsChanged 方法

    public void onWindowFocusChanged(boolean hasWindowFocus) {
       super.onWindowFocusChanged(hasWindowFocus);
       if(hasWindowFocus){
       int width=view.getMeasuredWidth();
       int height=view.getMeasuredHeight();
      }      
    }
    

    onWindowFocusChanged 方法表示 View 已經初始化完畢了,寬高已經準備好了,這個時候去獲取是沒問題的。這個方法會被調用多次,當 Activity 繼續執行或者暫停執行的時候,這個方法都會被調用。

  2. View.post(runnable)

    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width=view.getMeasuredWidth();
                int height=view.getMeasuredHeight();
            }
        });
    }
    

    用post異步加入到消息隊列,這樣也是可以獲取到的。

  3. ViewTreeObsever

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width=view.getMeasuredWidth();
                int height=view.getMeasuredHeight();
            }
        });
    }
    

    開一個子線程,觀察ViewTreeObserver 的回調也可以獲取view的寬高。當 View 樹的狀態發生改變或者 View 樹內部的 View 的可見性發生改變時,onGlobalLayout 方法將被回調。伴隨著View樹的變化,這個方法也會被多次調用。

    ?

二、layout過程

layout 的作用是 ViewGroup 來確定子元素的位置,當 ViewGroup 的位置被確定后,在 layout 中會調用 onLayout ,在 onLayout 中會遍歷所有的子元素并調用子元素的 layout 方法,在子元素的 layout 方法中 onLayout 方法又會被調用,layout 方法是確定 View 本身在屏幕上顯示的具體位置。

2.1 layout

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        // 即初始化四個頂點的值,然后判斷當前View大小和位置是否發生了變化并返回
        //第1步,調用 setFrame 方法 設置新的 mLeft、mTop、mBottom、mRight 值,
        //設置 View 本身四個頂點位置,并返回 changed 用于判斷 view 布局是否改變
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //第二步,如果 view 位置改變那么調用 onLayout 方法設置子 view 位置
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //調用 onLayout,是一個空方法。
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

即初始化四個頂點的值,然后判斷當前View大小和位置是否發生了變化并返回.

大致流程是,首先先調用setFrame(),設置View本身的4個點,其中setOpticalFrame本身內部也是調用setFrame(),所以最終都是通過調用setFrame()來設置view的4個點。View 的四個頂點一旦確定,那么 View 在父容器中的位置就確定了。然后第二步是onLayout,開始具體布局。其實onLayout是一個空方法,是要我們繼承重寫的。

2.1 onLayout

 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 }

三、 draw過程

draw過程總共分6步,實際是4步。

  1. Draw the background 繪制view背景
  2. If necessary, save the canvas' layers to prepare for fading
  3. Draw view's content 繪制view內容
  4. Draw children 繪制子View
  5. If necessary, draw the fading edges and restore layers
  6. Draw decorations (scrollbars for instance) 繪制裝飾(漸變框,滑動條等等
public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
        
        
        // 第一步
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        
         // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }
        。。。
        
         // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

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

推薦閱讀更多精彩內容