Android源码解析系列1——Activity启动和界面加载

源码版本:Android 27

一、应用的启动

首先我们需要知道:

ActivityThreadmain方法,是Android应用程序启动时的入口点。

public final class ActivityThread {
    // 省略部分代码

    public static void main(String[] args) {
        // 省略部分代码

        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}

在main方法里执行了以下操作:

  • 1、首先调用了Looper.prepareMainLooper()方法,它会创建一个与当前线程(主线程)相关联的Looper对象。
  • 2、然后创建一个ActivityThread对象,并调用其attach()方法。
  • 3、最后调用Looper.loop()方法,让刚创建的Looper对象,从它的MessageQueue中循环读取数据并执行。

现在,我们开始分析ActivityThreadattach()方法做了什么?

public final class ActivityThread {
    final ApplicationThread mAppThread = new ApplicationThread();
    // 省略部分代码

    private void attach(boolean system) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        if (!system) {
            // 省略部分代码

            RuntimeInit.setApplicationObject(mAppThread.asBinder());
            final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
            // 省略后续代码
        }
    }
}

attach方法中,调用ActivityManager.getService()方法,获取到远程的IActivityManager对象,并将一个ApplicationThread实例传入。

ApplicationThread继承至IApplicationThread.Stub,即Binder进程间通信的本地实现类。它有很多与Activity生命周期相关的方法,大都以schedule开头:

image.png

至此,我们可以总结一下:

1、ActivityThreadmain方法,是应用程序启动的入口。
2、Activity生命周期是由系统底层控制,通过Binder机制,回调到ApplicationThreadscheduleXXX方法中。
3、ActivityThreadApplicationThread针对每个应用,都只有一个实例。

这里有个问题:
为什么没看到scheduleStartActivity方法?难道Activity的onStart()生命周期回调,不是由ActivityManager发送消息控制的?

带着这个疑问,我们开始阅读这些scheduleXXX方法的源码。

二、Activity生命周期

我们从ApplicationThread中的scheduleLaunchActivity方法开始分析,因为由名字可以猜测它应该会执行Activity创建和启动工作。

public final class ActivityThread {
    // 省略部分代码

    private class ApplicationThread extends IApplicationThread.Stub {
        // 省略部分代码

        @Override
        public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                int procState, Bundle state, PersistableBundle persistentState,
                List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {

            updateProcessState(procState, false);

            ActivityClientRecord r = new ActivityClientRecord();

            // 给ActivityClientRecord赋值

            updatePendingConfiguration(curConfig);

            sendMessage(H.LAUNCH_ACTIVITY, r);
        }
        // 省略部分代码
    }
}

scheduleLaunchActivity方法中,首先创建了一个ActivityClientRecord对象,并对其进行赋值。然后通过Handler,将线程由Binder线程切换到主线程。最终调用到ActivityThreadhandleLaunchActivity方法。

注意:ActivityClientRecord主要用于记录与Activity相关的数据。

public final class ActivityThread {
    // 省略部分代码

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
        // 省略部分代码

        Activity a = performLaunchActivity(r, customIntent);

        if (a != null) {
            // 省略部分代码
            handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

           if (!r.activity.mFinished && r.startsNotResumed) {
                // The activity manager actually wants this one to start out paused, because it
                // needs to be visible but isn't in the foreground. We accomplish this by going
                // through the normal startup (because activities expect to go through onResume()
                // the first time they run, before their window is displayed), and then pausing it.
                // However, in this case we do -not- need to do the full pause cycle (of freezing
                // and such) because the activity manager assumes it can just retain the current
                // state it has.
                performPauseActivityIfNeeded(r, reason);
                // 省略部分代码
            }
        }
    }
}

这里,我们暂时不管performLaunchActivity方法中做了什么,仅分析后续代码。后续代码中,调用了handleResumeActivity,猜测它应该会调用Activity的onResume方法。

根据Activity生命周期推测到:

performLaunchActivity方法里,一定会依次调用Activity的onCreateonStart方法。

带着这个思路,开始分析performLaunchActivity方法。

public final class ActivityThread {
    // 省略部分代码

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // 省略部分代码

        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            // 省略部分代码
        } catch (Exception e) { /* 省略部分代码 */ }

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            // 省略部分代码

            if (activity != null) {
                // 省略部分代码

                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                appContext.setOuterContext(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, window, r.configCallback);
                // 省略部分代码

                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                // 省略部分代码

                if (!r.activity.mFinished) {
                    activity.performStart();
                    r.stopped = false;
                }
                if (!r.activity.mFinished) {
                    activity.mCalled = false;
                    if (r.isPersistable()) {
                        mInstrumentation.callActivityOnPostCreate(activity, r.state,
                                r.persistentState);
                    } else {
                        mInstrumentation.callActivityOnPostCreate(activity, r.state);
                    }
                    // 省略部分代码
                }
            }
            r.paused = true;

            // 保存ActivityClientRecord
            mActivities.put(r.token, r);

        } catch  { /* 省略catch相关代码 */ }

        return activity;
    }

上述代码主要执行了以下操作:

  • 1、创建Activity对象

调用InstrumentationnewActivity方法,通过反射创建Activity对象。

  • 2、初始化Activity

调用Activity对象的attach方法,用于初始化Activity的一些数据,同时会为Activity设置Window对象。注意:Activity的Window对象,与传入的Window对象不是同一个对象。这也意味着:每个Activity都有各自的Window对象。

public class Activity extends .... {
    // 省略部分代码
    private Window mWindow;
    private WindowManager mWindowManager;

    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, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        // 省略部分代码

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        // 省略部分代码

        mWindowManager = mWindow.getWindowManager();
        // 省略部分代码
    }
    // 省略部分代码
}
  • 3、调用3个生命周期方法

1、Instrumentation.callActivityOnCreate方法,该方法中会调用activity.performCreate()方法。
2、activity.performStart()方法。
3、Instrumentation.callActivityOnPostCreate方法,该方法中会调用activity.onPostCreate()方法。

查看里面的源码,确实依次调用了onCreate、onStart、onPostCreate方法,验证了我们之前对performLaunchActivity的猜想。

总结一下
handleLaunchActivity方法里,会回调以下生命周期:

onCreate() -> onStart() -> onPostCreate() -> onResume()

注意:如果ActivityClientRecord.startsNotResumed = true时,生命周期流程将会变为:
onCreate() -> onStart() -> onPostCreate() -> onResume()-> onPause()


二、界面加载

通过上节内容的介绍,我们知道在handleLaunchActivity方法里,会回调Activity的onCreate()生命周期方法。

而一般我们在创建Activity时,会在onCreate()中调用setContentView来设置布局文件。

下面,我们开始分析,我们自定义的布局文件是如何被加载出来的。

2.1 设置布局

首先分析Activity中的setContentView方法的源码。

public class Activity extends .... {
    // 省略部分代码

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    // 省略部分代码
}

根据前面的分析可知,这里的getWindow()方法返回的是该Activity所特有的Window对象,它在attach方法中被赋值。

Window是一个抽象类,通过查看类上的注解可知,它只有一个名为PhoneWindow的子类,所以我们直接看PhoneWindowsetContentView方法。

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    // 省略部分代码
    ViewGroup mContentParent;

    @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;
    }
    // 省略部分代码
}

代码还是比较清楚的,如果mContentParent == null,调用installDecor()方法,后续将传入的布局资源,加载到mContentParent中。

所以这里可以肯定:installDecor()方法,主要作用就是为了创建mContentParent对象。

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    // 省略部分代码
    private DecorView mDecor;
    ViewGroup mContentParent;

    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            // 省略部分代码
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            // 省略后续设置icon、title等代码
        }
    }
}

嗯,这里确实通过generateLayout方法创建了mContentParent对象,但在创建之前,先创建了一个DecorView对象,并将其作为参数传入generateLayout方法里。

DecorView,我们只需要知道它继承至FrameLayout即可,因为此时分析它的细节,对于我们并没太大帮助。

那我们分析generateLayout做了什么:

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    // 省略部分代码
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.

        // 省略部分代码:从主题文件中读取内容,并设置对应的Flag

        // Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
        //省略部分代码:通过features,为layoutResource设置不同布局资源id

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        //省略部分代码:为mDecor设置background、elevation、title、titleColor等数据

        mDecor.finishChanging();

        return contentParent;
    }
}

主要做了以下内容:

  • 1、从主题中获取数据,并应用到当前Window中。
  • 2、根据features,让mDecor加载不同的布局文件。
  • 3、获得mContentParent对象(id为com.android.internal.R.id.content)。

这里我们可以得出以下信息:

  • 1、不同的主题会让Window加载不同的布局到DecorView中。
  • 2、setContentView方法,实际是将自定义的布局文件,加载到mContentParent中。

至此,我们可以简单总结一下setContentView的流程:

1、首先,Activity中的Window对象会创建一个DecorView
2、然后根据不同的主题,让DecorView加载不同的布局资源。
3、获取这些布局资源中的mContentParent,它的id为com.android.internal.R.id.content
4、最后将自定义的布局加载到mContentParent中。

2.2 渲染布局

在上述setContentView的流程中,所有的布局资源都已加载完毕,而布局的加载又会涉及到addView方法。

一般来说,调用addView方法,都会间接调用到requestLayout()invalidate(true)方法,造成界面重新布局和刷新。

而我们也知道:

Activity在onCreate时,界面并不会被加载出来。

这里仿佛出现了矛盾,那我们再分析分析,看看为什么调用了addView方法后,界面却没有被加载出来。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    // 省略部分代码

    public void addView(View child, int index, LayoutParams params) {
        // 省略部分代码
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }
    // 省略部分代码
}
public class View implements ... {
    // 省略部分代码

    public void requestLayout() {
        // 省略部分代码
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        // 省略部分代码
    }

    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        // 省略部分代码
        if (skipInvalidate()) {
            return;
        }
        // 省略后续代码
    }

    private boolean skipInvalidate() {
        return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
                (!(mParent instanceof ViewGroup) ||
                        !((ViewGroup) mParent).isViewTransitioning(this));
    }
    // 省略部分代码
}

哦,原来此时DecorView没有父容器,导致这里只会执行添加操作,而不会去重新布局和刷新。


那什么时候,界面才会被加载出来呢?

只要学过Android生命周期的人,都知道:

当Activity处于onCreate时,是不可见的。
当Activity处于onStart时,可见,但不可交互,
当Activity处于onResume时,才是可见可交互的。

那我们看看ActivityperformStart方法实现:

public class Activity extends .... {
    // 省略部分代码

    final void performStart() {
        mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions());
        mFragments.noteStateNotSaved();
        mCalled = false;
        mFragments.execPendingActions();
        mInstrumentation.callActivityOnStart(this);
        if (!mCalled) {
            throw new SuperNotCalledException(
                "Activity " + mComponent.toShortString() +
                " did not call through to super.onStart()");
        }
        mFragments.dispatchStart();
        mFragments.reportLoaderStart();

       // 省略部分代码

        mActivityTransitionState.enterReady(this);
    }

这里只要调用了InstrumentationcallActivityOnStart方法,而callActivityOnStart方法内部的实现,只是简单调用了传入的Activity对象的onStart()方法。

emmmmm......好像有点不对啊,performStart方法里好像也没做什么操作啊,难不成界面渲染是在onResume里?

带着疑惑,我们一起来看看handleResumeActivity方法:

public final class ActivityThread {
    // 省略部分代码

    final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
        ActivityClientRecord r = mActivities.get(token);
        // 省略部分代码

        // TODO Push resumeArgs into the activity for consideration
        r = performResumeActivity(token, clearHide, reason);

        if (r != null) {
            final Activity a = r.activity;
            // 省略部分代码

            if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                // 省略部分代码

                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        // The activity will get a callback for this {@link LayoutParams} change
                        // earlier. However, at that time the decor will not be set (this is set
                        // in this method), so no action will be taken. This call ensures the
                        // callback occurs with the decor set.
                        a.onWindowAttributesChanged(l);
                    }
                }
                // 省略后续代码
            }
        }
    }
}

其中performResumeActivity也没做过多操作,只是调用了Activity的performResume()方法,间接调用到onResume,我们就不过多分析了。
这里比较核心的是,将DecorView添加到ViewManager中,这里间接调用到WindowManagerGlobaladdView方法,它是一个单例对象,注意这里的判定条件,可以看出,一个ActivityClientRecord对象,之后执行一次。

public final class WindowManagerGlobal {
    // 省略部分代码
    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();

    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        // 省略部分代码

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            // 省略部分代码

            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);

            // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }
    // 省略部分代码
}

这里先创建ViewRootImpl对象,然后进行缓存到数组中。接下来把DecorView设置到ViewRootImpl中,进而执行到ViewRootImplrequestLayout() -> scheduleTraversals() -> doTraversal()方法,最终执行到performTraversals()方法中,流程图如下:

image.png

View的绘制流程,会涉及到测量摆放绘制三部分,这个我们后面单独在View渲染章节来讲。我们只需明白,此时界面就已经被渲染出来了。

2.3、延伸

现在,我们已经知道了,Activity渲染到屏幕,是在onResume()之后才完成的,那为啥说,onStart()可见但不可交互的呢?
这里就不卖关子了,其实官方所说的"可见",其实有一定的歧义,我认为:

"可见"只是针对于已被渲染过的Activity,而不是正在创建的Activity。

参照下面这张图来解释一下:

image.png

1、对于创建的Activity,只是简单的走了生命周期,渲染是在Resumed时完成的。
2、对于已被渲染过的Activity:

1、当它由Resumed状态切换到Started状态,界面被部分覆盖,失去焦点,即无法进行交互。
2、当它由Started状态切换到Created状态,界面被完全覆盖,即不可见。
3、当它由Created状态切换到Started状态,界面再次被部分覆盖,依然获取不到焦点,无法交互。
4、当它由Started状态切换到Resumed状态,界面完全显示。

正是因为这样,才会造成以下这个的问题:

Activity创建过程中,在onCreate()onStart()onResume()方法里,都无法获取控件的宽高。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容