Android Window源碼分析

在Android中所有的視圖都是通過Window來呈現(xiàn)的壮啊,Window是View的直接管理者关翎,每一個Activity都對應著一個Window,Activity的視圖DecorView會被添加到其Window中靡砌;另外奔誓,如果我們想要實現(xiàn)懸浮窗的效果嘹锁,那么也離不開Window的開發(fā)膜赃。Android為我們提供了WindowManager類可以用來管理Window由缆,WindowManager可以通過Activity的getWindowManager()獲得汗盘,WindowManager實現(xiàn)了ViewManager接口皱碘,這個接口有三個方法:addView()、updateViewLayout()隐孽、removeView()癌椿,通過這三個方法就可以完成Window的添加、更新和刪除操作菱阵,接下來我們分別看看這三個方法的執(zhí)行流程踢俄。

Window的添加過程

我們通過Activity對Window的處理來分析下Window的添加流程。在啟動一個Activity時晴及,會調用到ActivityThread的performLaunchActivity()創(chuàng)建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) {
    ...
    mWindow = new PhoneWindow(this); 
    ...
    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    ...
    mWindowManager = mWindow.getWindowManager();
    ...
}

該方法首先會創(chuàng)建一個Window實例并賦值給mWindow,Window 是個抽象的概念, 具體實現(xiàn)類是 PhoneWindow琳钉,接著會獲取一個WindowManager對象势木,WindowManager是一個接口類型,具體實現(xiàn)是WindowManagerImpl歌懒,最后調用Window.setWindowManager()給Window設置WindowManager對象啦桌,如下所示:

public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    ...
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

方法內通過調用WindowManager的createLocalWindowManager()創(chuàng)建了一個新的WindowManager對象并賦值給mWindowManager,該方法內部直接new了一個WindowManagerImpl對象及皂,因此可以知道每個Window都會對應一個WindowManagerImpl對象甫男,方法如下所示:

public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    return new WindowManagerImpl(mContext, parentWindow);
}

在Activity初始化完成后會調用Activity的onCreate(),而調用onCreate()時會調用setContentView()設置布局验烧,其內部會調用剛剛創(chuàng)建的Window的setContentView()板驳,如下所示:

public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }
    ...
}

可以看到內部會調用installDecor(),該方法會根據不同的Theme創(chuàng)建不同的DecorView碍拆,DecorView 是一個 FrameLayout若治,setContentView()最終會把布局設置到DecorView中id為content的View上。到這里創(chuàng)建了 PhoneWindow和DecorView倔监,但目前二者也沒有任何關系直砂。當Activity的狀態(tài)變成resume時,最終會調用到ActivityThread的handleResumeActivity()浩习,如下所示:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        boolean willBeVisible = !a.mStartedActivity;
        ...
        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;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                wm.addView(decor, l);
            }
        }
        ...
}

該方法首先會調用performResumeActivity()執(zhí)行Activity的onResume(),接著獲取到前面創(chuàng)建的WindowManagerImpl對象并調用其addView()創(chuàng)建DecorView對應的Window济丘,接下來看下WindowManagerImpl的內部實現(xiàn):

public final class WindowManagerImpl implements WindowManager {
    public void addView(View view, ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.updateViewLayout(view, params);
    }

    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }
    ...
}

WindowManagerImpl實現(xiàn)了WindowManager的方法谱秽,但并沒有具體實現(xiàn)具體的操作,而是調用WindowManagerGlobal對應的方法來完成摹迷,接下來看一下WindowManagerGlobal的addView():

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    ...
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);
    ...
    root.setView(view, wparams, panelParentView);
    ...
}

這個過程創(chuàng)建一個ViewRootImpl疟赊,并將View、ViewRootImpl以及LayoutParams等參數(shù)保存到一個列表中峡碉。最后會調用ViewRootImpl的setView()近哟,如下所示:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
    mView = view;
    ...
    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
          getHostVisibility(), mDisplay.getDisplayId(),
          mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
          mAttachInfo.mOutsets, mInputChannel);
    ...
    view.assignParent(this);
    ...
}

該方法首先將傳入的View賦值給ViewRootImpl的成員變量mView,這里傳入的View是DecorView鲫寄,這樣DecorView就交由ViewRootImpl進行管理了吉执;接著調用mWindowSession.addToDisplay(),mWindowSession是個Binder對象地来,會與WMS通信由WMS完成Window的創(chuàng)建戳玫;最后會調用傳入的View的assignParent(),該方法會將View的mParent設為當前的ViewRootImpl未斑,mParent是ViewParent類型咕宿,ViewRootImpl和ViewGroup都實現(xiàn)了ViewParent接口,當ViewGroup(如DecorView)調用addView()添加子View時,會將子View的mParent置為該ViewGroup府阀,這樣就形成了ViewRootImpl -> DecorView -> ViewGroup -> ... -> View這樣的樹形結構缆镣。ViewParent接口最常見的一個方法是requestLayout(),當調用View或ViewGroup的requestLayout()時试浙,會找到View樹的根節(jié)點ViewRootImpl费就,執(zhí)行ViewRootImpl的requestLayout(),執(zhí)行該方法時會依次執(zhí)行performMeasure()川队、performLayout()力细、performDraw(),它們的內部會分別調用mView的measure()固额、layout()眠蚂、draw(),上面已經說到這里的mView就是DecorView斗躏,DecorView會遍歷其子View向下傳遞事件逝慧,這樣一個View樹的工作流程就啟動了。
ViewRootImpl可以理解成是WindowManager和DecorView的紐帶啄糙,它們的關系如下如所示:


28782908-7672-4693-a47e-ee83f717532c.png

接下來看一下WMS部分的處理邏輯笛臣,在調用IWindowSession.addToDisplay()后,會調用到Session的addToDisplay()隧饼,其內部又調用了WindowManagerService.addWindow()沈堡,該方法主要做了以下幾件事:

  • 對所要添加的窗口進行權限及類型的檢查,如果窗口不滿足一些條件燕雁,就不會再執(zhí)行下面的代碼邏輯诞丽。
  • WindowToken相關的處理,比如有的窗口類型需要提供WindowToken拐格,沒有提供的話就不會執(zhí)行下面的代碼邏輯僧免,有的窗口類型則需要由WMS隱式創(chuàng)建默認windowToken。
  • WindowState的創(chuàng)建和相關處理捏浊,將WindowToken和WindowState相關聯(lián)懂衩。WindowState存有窗口的所有的狀態(tài)信息,在WMS中它表示一個窗口金踪。
  • 創(chuàng)建和配置DisplayContent浊洞,完成窗口添加到系統(tǒng)前的準備工作。

在創(chuàng)建WindowState時會傳入一個IWindow類型的對象作為參數(shù)热康,這是一個Binder對象沛申,IWindow實現(xiàn)是ViewRootImpl的內部類W,IWindow會將WMS中窗口管理的操作回調給ViewRootlmpl姐军,這樣一個Window就創(chuàng)建完成了铁材,創(chuàng)建Window過程中涉及的各個對象也都建立起了聯(lián)系尖淘。

根據上面的分析可以知道創(chuàng)建一個Window的核心在于調用WindowManager.addView()并將Window的視圖傳遞進去;接著會為Window創(chuàng)建一個ViewRootImpl著觉,后續(xù)Window視圖的事件都交由ViewRootImpl進行管理村生;最后與WMS通信完成Window創(chuàng)建。在Activity的Window創(chuàng)建中視圖正是DecorView饼丘。而如果我們需要創(chuàng)建一個子窗口趁桃,則需要先創(chuàng)建一個View,再調用WindowManager.addView()即可肄鸽。但是我們看過源碼后發(fā)現(xiàn)卫病,調用WindowManager的addView()并不會創(chuàng)建一個PhoneWindow類型的對象,那么為什么窗口會創(chuàng)建了典徘?或者說PhoneWindow的職責究竟是什么蟀苛?
我理解是表示一個窗口的并不是應用內的PhoneWindow對象,而是WMS內的WindowState對象逮诲,前文說到了WindowState有窗口的全部狀態(tài)信息帜平,而且無論是Activity、Dialog的窗口或是我們創(chuàng)建的子窗口梅鹦,都會調用WindowManager.addView()裆甩,其最終會調用到WMS.addWindow()創(chuàng)建WindowState對象用于表示窗口。既然Window并不真正表示一個窗口齐唆,那么PhoneWindow的作用是什么呢嗤栓?

  • PhoneWindow的一個作用是給view包裹上一層DecorView,而DecorView中的布局結構會根據Theme決定蝶念。
  • 我們的Activity和Dialog的布局都比較復雜抛腕,比如都可能有appbar(toolbar/actionbar)等,通過PhoneWindow來封裝下可以更好的解耦代碼媒殉。

Dialog和Window一樣都使用了PhoneWindow封裝了DecorView,因此Dialog的樣式也會根據Theme決定摔敛,但是PopupWindow以及Toast同樣是Window廷蓉,其內部并沒有使用PhoneWindow,而是直接通過WindowManager.addWindow()創(chuàng)建的Window马昙,主要原因是PopupWindow和Toast樣式相對較簡單桃犬,無需通過PhoneWindow進行一層封裝。

Window的刪除過程

在了解了Window的添加過程后行楞,我們依舊是通過Activity對Window的處理來看下Window的刪除過程攒暇。在Activity銷毀時會調用ActivityThread.handleDestroyActivity(),如下所示:

private void handleDestroyActivity(IBinder token, boolean finishing,
        int configChanges, boolean getNonConfigInstance) {
    ActivityClientRecord r = performDestroyActivity(token, finishing,
            configChanges, getNonConfigInstance);
    if (r != null) {
        cleanUpPendingRemoveWindows(r, finishing);
        WindowManager wm = r.activity.getWindowManager();
        View v = r.activity.mDecor;
        if (v != null) {
            ...
            wm.removeViewImmediate(v);
            ...
        }
        ...
}

首先獲得了WindowManager以及Activity的DecorView子房,接著調用WindowManager的removeViewImmediate()并將DecorView作為參數(shù)傳入形用,removeViewImmediate()的實現(xiàn)位于WindowManagerImpl中就轧,如下所示:

public void removeView(View view) {
    mGlobal.removeView(view, false);
}

public void removeViewImmediate(View view) {
    mGlobal.removeView(view, true);
}

removeViewImmediate()同樣沒有具體實現(xiàn)操作,也是直接調用WindowManagerGlobal.removeView()田度,另外除了removeViewImmediate()妒御,還有前面說到的ViewManager中的removeView(),這兩個接口都是用于刪除Window并都會調用WindowManagerGlobal.removeView()镇饺,但區(qū)別在于前者調用時immediate為true乎莉,這個字段標識是否要立即銷毀Window,我們后面會講到奸笤。接下來看一下WindowManagerGlobal的removeView()惋啃,其內部會找到傳入的View在列表中的索引并調用removeViewLocked(),如下所示:

private void removeViewLocked(int index, boolean immediate) {
    ViewRootImpl root = mRoots.get(index);
    View view = root.getView();
    if (view != null) {
        InputMethodManager imm = InputMethodManager.getInstance();
        if (imm != null) {
            imm.windowDismissed(mViews.get(index).getWindowToken());
        }
    }
    boolean deferred = root.die(immediate);
    if (view != null) {
        view.assignParent(null);
        if (deferred) {
            mDyingViews.add(view);
        }
    }
}

首先會獲取InputMethodManager并調用windowDismissed()來結束Window輸入法相關邏輯监右,接著會調用ViewRootImpl.die()并傳入了前文說到的immediate參數(shù)边灭,方法如下所示:

boolean die(boolean immediate) {
    if (immediate && !mIsInTraversal) {
        doDie();
        return false;
    }
    if (!mIsDrawing) {
        destroyHardwareRenderer();
    } else {
        Log.e(mTag, "Attempting to destroy the window while drawing!\n" +
                "  window=" + this + ", title=" + mWindowAttributes.getTitle());
    }
    mHandler.sendEmptyMessage(MSG_DIE);
    return true;
}

首先若immediate為true且mIsInTraversal為false則會執(zhí)行doDie()并返回,immediate是前文傳遞下來的秸侣,若我們執(zhí)行的是WindowManager.removeViewImmediate()則該值為true存筏,mIsInTraversal會在ViewRootImpl執(zhí)行performTraversals()時置為true,執(zhí)行結束后置為false味榛,因此這里的邏輯就是如果要立即執(zhí)行(immediate為true)且ViewRootImpl不再執(zhí)行performTraversals()時會執(zhí)行doDie()椭坚,否則會通過Handler發(fā)送一個MSG_DIE消息,而Handler在處理這個消息時就會執(zhí)行doDie()搏色,因此我們看下doDie()的實現(xiàn)善茎,如下所示:

void doDie() {
    ...
    if (mAdded) {
        dispatchDetachedFromWindow();
    }
    ...
    WindowManagerGlobal.getInstance().doRemoveView(this);
}

上述代碼主要關注兩個地方,首先是要刪除的Window如果有子View會調用dispatchDetachedFromWindow()來銷毀View频轿,接著調用WindowManagerGlobal的doRemoveView()垂涯,我們先看下WindowManagerGlobal的doRemoveView(),如下所示:

void doRemoveView(ViewRootImpl root) {
    synchronized (mLock) {
        final int index = mRoots.indexOf(root);
        if (index >= 0) {
            mRoots.remove(index);
            mParams.remove(index);
            final View view = mViews.remove(index);
            mDyingViews.remove(view);
        }
    }
    if (ThreadedRenderer.sTrimForeground && ThreadedRenderer.isAvailable()) {
        doTrimForeground();
    }
}

可以看到該方法主要是從列表中移除了與要刪除的Window對應的View航邢、ViewRootImpl和LayoutParams耕赘,這幾個列表在前面的添加過程也看到過。接著我們看下dispatchDetachedFromWindow()的實現(xiàn)膳殷,代碼如下:

void dispatchDetachedFromWindow() {
    ...
    mWindowSession.remove(mWindow);
    ...
}

dispatchDetachedFromWindow()會進行一些資源的釋放操骡,接著調用了IWindowSession類型的remove(),IWindowSession在添加過程中已經看到過赚窃,它用于與WMS通信册招,會調用Session.remove(),其內部又會調用WindowManagerService.removeWindow()勒极,該方法會通過Session和IWindow獲取到對應的WindowState是掰,并調用WindowState的removeIfPossible(),最終會調用到WindowState的removeImmediately()辱匿,該方法主要會對Window涉及的一些資源進行回收與清理键痛,到這里Window的刪除過程就完成了炫彩。

Window的更新過程

我們要更新Window時,會調用WindowManager.updateViewLayout()散休,這個方法的實現(xiàn)在WindowManagerImpl中媒楼,代碼如下所示:

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.updateViewLayout(view, params);
}

該方法依舊是調用WindowManagerGlobal.updateViewLayout()處理,代碼如下所示:

public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
    ...
    WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
    view.setLayoutParams(wparams);
    synchronized (mLock) {
        int index = findViewLocked(view, true);
        ViewRootImpl root = mRoots.get(index);
        mParams.remove(index);
        mParams.add(index, wparams);
        root.setLayoutParams(wparams, false);
    }
}

首先是根據View找到要更新的Window的索引戚丸,再根據索引更新列表中LayoutParams划址,接著調用與Window對應的ViewRootImpl的setLayoutParams(),其內部會調用scheduleTraversals()限府,如下所示:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //1
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

注釋1處的代碼添加了一個mTraversalRunnable夺颤,mTraversalRunnable是TraversalRunnable類型,它實現(xiàn)了Runnable接口胁勺,run()里的代碼在下一幀渲染時會被執(zhí)行世澜,我們看下mTraversalRunnable的代碼,如下所示:

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

因此在下一幀渲染時會執(zhí)行doTraversal()署穗,其內部又會調用performTraversals()寥裂,performTraversals()其實我們就很清楚了,它會執(zhí)行View的measure案疲、layout以及draw流程封恰,因此Window里的視圖會執(zhí)行上述的流程,但performTraversals()除了執(zhí)行View的工作流程外褐啡,還會調用relayoutWindow()诺舔,代碼如下所示:

private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
    ...
    int relayoutResult = mWindowSession.relayout(
            mWindow, mSeq, params,
            (int) (mView.getMeasuredWidth() * appScale + 0.5f),
            (int) (mView.getMeasuredHeight() * appScale + 0.5f),
            viewVisibility, insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,
            mWinFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
            mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame,
            mPendingMergedConfiguration, mSurface);
    ...
}

這里的mWindowSession已經見過很多次了,它是個IWindowSession類型的Binder對象备畦,用于與WMS通信低飒,會調用到Session.relayout(),其內部會調用WindowManagerService的relayoutWindow()懂盐,這樣就完成了Window的更新操作褥赊。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市莉恼,隨后出現(xiàn)的幾起案子崭倘,更是在濱河造成了極大的恐慌,老刑警劉巖类垫,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異琅坡,居然都是意外死亡悉患,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門榆俺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來售躁,“玉大人坞淮,你說我怎么就攤上這事∨憬荩” “怎么了回窘?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長市袖。 經常有香客問我啡直,道長,這世上最難降的妖魔是什么苍碟? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任酒觅,我火速辦了婚禮,結果婚禮上微峰,老公的妹妹穿的比我還像新娘舷丹。我一直安慰自己,他們只是感情好蜓肆,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布颜凯。 她就那樣靜靜地躺著,像睡著了一般仗扬。 火紅的嫁衣襯著肌膚如雪症概。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天厉颤,我揣著相機與錄音穴豫,去河邊找鬼。 笑死逼友,一個胖子當著我的面吹牛精肃,可吹牛的內容都是我干的。 我是一名探鬼主播帜乞,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼司抱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了黎烈?” 一聲冷哼從身側響起习柠,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎照棋,沒想到半個月后资溃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡烈炭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年溶锭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片符隙。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡趴捅,死狀恐怖垫毙,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情拱绑,我是刑警寧澤综芥,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站猎拨,受9級特大地震影響膀藐,放射性物質發(fā)生泄漏。R本人自食惡果不足惜迟几,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一消请、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧类腮,春花似錦臊泰、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至厂抽,卻和暖如春需频,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背筷凤。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工昭殉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人藐守。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓挪丢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親卢厂。 傳聞我的和親對象是個殘疾皇子乾蓬,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344