android xml轉(zhuǎn)換為view過程

前言:
Android布局文件Xml侣诺,通過setContentView(@LayoutResint layoutResID)或者LayoutInflater.from(context).inflate(int ResID)轉(zhuǎn)換為Java對象铲咨,開發(fā)工具Android Studio 提供的預(yù)覽功能众旗,開發(fā)過程中界面和業(yè)務(wù)可以并行開發(fā)姆蘸,提高了開發(fā)效率。以下分析過程是基于 Android API 25 Platform 源碼宾尚,并以setContentView()方法為入口孝情。

Xml 轉(zhuǎn)成 Java 對象方式

1、Activity中setContentView(@LayoutResint layoutResID)方法著恩;該方法都會被每個(gè)繼承 android.app.Activity 的子類重載院尔;

2、LayoutInflater.from(Context context).inflate(@LayoutResint resource, ...)喉誊。

一般使用的 Activity 可能是

1). android.support.v7.app.AppCompatActivity
2). android.support.v4.app.FragmentActivity
3). android.app.Activity
4). 其他 Activity

從Activity中setContentView()方法開始

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
}

跟蹤下getWindow()源碼:

public Window getWindow() {
        return mWindow;
    }

mWindow在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,
            Window window) {
        ...
        mWindow = new PhoneWindow(this, window);
        ...
    }

所以Window.java的實(shí)現(xiàn)類是PhoneWindow.java類邀摆,@hide代表 PhoneWindow 的源碼在 sdk 里面是隱藏的,查看 PhoneWindow.setContentView(layoutResID)如下:

@Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        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);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

從上面代碼可以發(fā)現(xiàn)如果沒有轉(zhuǎn)場動畫時(shí)伍茄,執(zhí)行的是

mLayoutInflater.inflate(layoutResID, mContentParent);

在PhoneWindow構(gòu)造函數(shù)里發(fā)現(xiàn)mLayoutInflater對象賦值代碼如下:

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

所以可以得出一個(gè)結(jié)論 Activity.setContentView(resId) 最終還是使用LayoutInflater.from(context).inflate(resId, ……)栋盹。

在看下其他activity android.support.v7.app.AppCompatActivityandroid.support.v4.app.FragmentActivity 發(fā)現(xiàn) android.support.v4.app.FragmentActivity 沒有重載 android.app.Activity.setContentView(resId) 但是 android.support.v7.app.AppCompatActivity 重載了

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

getDelegate()源代碼最終會調(diào)用到 android.support.v7.app.AppCompatDelegateImplV9.setContentView(resId)

@Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }
因此xml 轉(zhuǎn)成 Java 對象是通過LayoutInflaterinflate()方法來完成的

關(guān)鍵字abstractLayoutInflater是一個(gè)抽象類敷矫,不能實(shí)例化例获,LayoutInflater 對象獲取的方式有:

1). 在 Activity 中通過 getLayoutInflater() 獲取
2). LayoutInflater里靜態(tài)方法from(context) 獲取
3). context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) 獲取

如 Activity 的 getLayoutInflater()

 /**
     * Convenience for calling
     * {@link android.view.Window#getLayoutInflater}.
     */
    @NonNull
    public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }

可以看出 Activity 通過 getLayoutInflater() 獲取的是 PhoneWindow 的 mLayoutInflater音念。

LayoutInflater.from(context)

   /**
     * 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;
    }
所以LayoutInflater對象都是通過服務(wù)獲取 LayoutInflater 實(shí)例對象

跟蹤下源碼context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
Context的實(shí)現(xiàn)類是ContextImpl.java,如:

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

繼續(xù)跟蹤 SystemServiceRegistry.java

  /**
     * 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;
    }

   /**
     * Statically registers a system service with the context.
     * This method must be called during static initialization only.
     */
    private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

在 SystemServiceRegistry 類躏敢,這里只注冊各種系統(tǒng)服務(wù)的處闷愤,通過 Context.LAYOUT_INFLATER_SERVICE找到注冊代碼地方,如下:

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

通過以上代碼發(fā)現(xiàn) LayoutInflater 的實(shí)現(xiàn)類是 PhoneLayoutInflater

LayoutInflater 讀取 Xml 文件并創(chuàng)建 View 對象件余,繼續(xù)跟蹤LayoutInflater.inflate()方法
1).View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
2).View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 

重點(diǎn)看第二個(gè)方法讥脐,代碼如下:

   /**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * {@link InflateException} if there is an error.
     * 
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    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) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

根據(jù)以上代碼邏輯,首先通過 resource 對象把 resId 指向的 xml 文件轉(zhuǎn)換為XmlResourceParser啼器,然后執(zhí)行inflate(parser, root, attachToRoot)方法旬渠,核心代碼如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root;

            try {
                // Look for the root node.
                ...
                final String name = parser.getName();
                //分析1
                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");
                    }
                    //分析2
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    //分析3
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {                   
                        // Create layout params that match root, if supplied
                        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);
                        }
                    }
                    //分析4
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            //異常處理部分
            return result;
        }
    }
分析1:

如果 Xml 根標(biāo)簽是 TAG_MERGE(即merge),則 root 不能為空端壳, attachToRoot 為 true告丢,在執(zhí)行rInflate(parser, root, inflaterContext, attrs, false)

分析2 rInflate()方法
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();
        }
    }

rInflate(parser, root, inflaterContext, attrs, false) 總結(jié)如下

1). while遍歷該節(jié)點(diǎn)的子節(jié)點(diǎn)
2). 子節(jié)點(diǎn)有 "requestFocus"、"tag"损谦、""岖免、"include" 
3). 子節(jié)點(diǎn)不能是 "merge"
4). 子節(jié)點(diǎn)的其他情況,則是各種 View 的標(biāo)簽
5). View 標(biāo)簽和  "include" 標(biāo)簽會創(chuàng)建 View 對象
6). 遍歷結(jié)束以后執(zhí)行 parent.onFinishInflate()

如果子節(jié)點(diǎn)是 include照捡,則執(zhí)行 parseInclude() ,parseInclude() 的源碼和 inflate(parser, root, attachToRoot) 類似颅湘,都是讀取xml對應(yīng)的文件,轉(zhuǎn)換成 XmlResourceParser 然后遍歷里的標(biāo)簽栗精。

createViewFromTag(parent, name, context, attrs)負(fù)責(zé)創(chuàng)建 View 對象
分析3闯参、4
1). root 不為 null,才會讀取 xml 跟布局的 params 屬性;
2). attachToRoot 為 True 悲立,返回的是  root 對象鹿寨。否則返回的是 xml 創(chuàng)建的根標(biāo)簽指定的 View
3). 調(diào)用了 createViewFromTag(root, name, inflaterContext, attrs) 方法創(chuàng)建 View 
4). rInflateChildren()->rInflate();和 分析2一樣的
綜上所述,LayoutInflater.createViewFromTag()創(chuàng)建 View 對象薪夕,源碼如下:
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        // Apply a theme wrapper, if allowed and one is specified.
        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();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        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];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
       //異常處理
       ...
    }

mFactory2脚草、mFactorymPrivateFactory 三個(gè)對象寥殖,似乎都是可以創(chuàng)建 View , 對于android.app.Activity玩讳,這三個(gè)對象為 null 或者空實(shí)現(xiàn),創(chuàng)建 View 對象直接看如下代碼:

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

注:如果 name屬性里面含有.表示這是一個(gè)自定義 View嚼贡,系統(tǒng)自帶 View 我們可以省略類的路徑熏纯,而自定義 View 則不能省略

自定義View創(chuàng)建,核心代碼如下:

public final View createView(String name, String prefix, 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 {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                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 we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        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;

            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;

        //異常處理
        ......
    }

以上代碼可以看出constructor.newInstance(args) 粤策,通過反射創(chuàng)建 View 對象

對于 Android 內(nèi)置的各種 View 在 LayoutInflater 的實(shí)現(xiàn)類PhoneLayoutInflater中重載了onCreateView()方法

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

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

LayoutInflater 中的代碼如下:

protected View onCreateView(View parent, String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return onCreateView(name, attrs);
    }

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

對于系統(tǒng)內(nèi)置的 View樟澜,會依次在 View 的標(biāo)簽前面加上android.widget.android.webkit.android.app. 秩贰,android.view. 然后通過反射的方法創(chuàng)建 View霹俺。

結(jié)束

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市毒费,隨后出現(xiàn)的幾起案子丙唧,更是在濱河造成了極大的恐慌,老刑警劉巖觅玻,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件想际,死亡現(xiàn)場離奇詭異,居然都是意外死亡溪厘,警方通過查閱死者的電腦和手機(jī)胡本,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來畸悬,“玉大人侧甫,你說我怎么就攤上這事√;拢” “怎么了披粟?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長妆档。 經(jīng)常有香客問我僻爽,道長,這世上最難降的妖魔是什么贾惦? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮敦捧,結(jié)果婚禮上须板,老公的妹妹穿的比我還像新娘。我一直安慰自己兢卵,他們只是感情好习瑰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秽荤,像睡著了一般甜奄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窃款,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天课兄,我揣著相機(jī)與錄音,去河邊找鬼晨继。 笑死烟阐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蜒茄,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼檀葛!你這毒婦竟也來了玩祟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤屿聋,失蹤者是張志新(化名)和其女友劉穎空扎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胜臊,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡勺卢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了象对。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黑忱。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖勒魔,靈堂內(nèi)的尸體忽然破棺而出甫煞,到底是詐尸還是另有隱情,我是刑警寧澤冠绢,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布抚吠,位于F島的核電站,受9級特大地震影響弟胀,放射性物質(zhì)發(fā)生泄漏楷力。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一孵户、第九天 我趴在偏房一處隱蔽的房頂上張望萧朝。 院中可真熱鬧,春花似錦夏哭、人聲如沸检柬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽何址。三九已至,卻和暖如春进胯,著一層夾襖步出監(jiān)牢的瞬間用爪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工龄减, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留项钮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像烁巫,于是被迫代替她去往敵國和親署隘。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容