Android深入四大組件(五)Content Provider的啟動(dòng)過程

相關(guān)文章
Android深入理解四大組件系列

前言

Content Provider做為四大組件之一,通常情況下并沒有其他的組件使用頻繁嚣艇,但這不能作為我們不去深入學(xué)習(xí)它的理由队橙。關(guān)于Content Provider一篇文章是寫不完的候味,這一篇文章先來介紹它的啟動(dòng)過程抒寂。

1.query方法到AMS的調(diào)用過程

Android IPC機(jī)制(四)用ContentProvider進(jìn)行進(jìn)程間通信這篇文章我舉了一個(gè)Content Provider使用的例子,在Activity中我是使用如下代碼調(diào)用Content Provider的:

public class ContentProviderActivity extends AppCompatActivity {
    private final static String TAG = "ContentProviderActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_content_provider);
        Uri uri = Uri.parse("content://com.example.liuwangshu.mooncontentprovide.GameProvider");
        ContentValues mContentValues = new ContentValues();
        mContentValues.put("_id", 2);
        mContentValues.put("name", "大航海時(shí)代ol");
        mContentValues.put("describe", "最好玩的航海網(wǎng)游");
        getContentResolver().insert(uri, mContentValues);//1
        Cursor gameCursor = getContentResolver().query(uri, new String[]{"name", "describe"}, null, null, null);
     ...
    }
}

要想調(diào)用Content Provider,首先需要使用注釋1處的getContentResolver方法沮榜,如下所示盘榨。
frameworks/base/core/Java/android/content/ContextWrapper.java

@Override
public ContentResolver getContentResolver() {
    return mBase.getContentResolver();
}

這里mBase指的是ContextImpl,ContextImpl的getContentResolver方法如下所示蟆融。

frameworks/base/core/java/android/app/ContextImpl.java

@Override
public ContentResolver getContentResolver() {
    return mContentResolver;
}

上面的代碼return了ApplicationContentResolver類型的mContentResolver對(duì)象草巡,ApplicationContentResolver是ContextImpl中的靜態(tài)內(nèi)部類,繼承自ContentResolver型酥,它在ContextImpl的構(gòu)造方法中被創(chuàng)建山憨。
當(dāng)我們調(diào)用ContentResolver的insert、query弥喉、update等方法時(shí)就會(huì)啟動(dòng)Content Provider郁竟,這里拿query方法來進(jìn)行舉例。
query方法的實(shí)現(xiàn)在ApplicationContentResolver的父類ContentResolver中由境,代碼如下所示棚亩。
frameworks/base/core/java/android/content/ContentResolver.java

public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder,
            @Nullable CancellationSignal cancellationSignal) {
        Preconditions.checkNotNull(uri, "uri");
        IContentProvider unstableProvider = acquireUnstableProvider(uri);//1
        ...
        try {
           ...
            try {
                qCursor = unstableProvider.query(mPackageName, uri, projection,
                        selection, selectionArgs, sortOrder, remoteCancellationSignal);//2
            } catch (DeadObjectException e) {
               ...
            }
 ...
    }

在注釋1處通過acquireUnstableProvider方法返回IContentProvider類型的unstableProvider對(duì)象,在注釋2處調(diào)用unstableProvider的query方法虏杰。我們查看acquireUnstableProvider方法做了什么讥蟆,如下所示。
frameworks/base/core/java/android/content/ContentResolver.java

   public final IContentProvider acquireUnstableProvider(Uri uri) {
        if (!SCHEME_CONTENT.equals(uri.getScheme())) {//1
            return null;
        }
        String auth = uri.getAuthority();
        if (auth != null) {
            return acquireUnstableProvider(mContext, uri.getAuthority());//2
        }
        return null;
    }

注釋1處用來檢查Uri的scheme是否等于"content"纺阔,如果不是則返回null瘸彤。注釋2處調(diào)用了acquireUnstableProvider方法,這是個(gè)抽象方法笛钝,它的實(shí)現(xiàn)在ContentResolver的子類ApplicationContentResolver中:
frameworks/base/core/java/android/app/ContextImpl.java

    @Override
    protected IContentProvider acquireUnstableProvider(Context c, String auth) {
        return mMainThread.acquireProvider(c,
                ContentProvider.getAuthorityWithoutUserId(auth),
                resolveUserIdFromAuthority(auth), false);
    }

return了ActivityThread類型的mMainThread對(duì)象的acquireProvider方法:
frameworks/base/core/java/android/app/ActivityThread.java

   public final IContentProvider acquireProvider(
            Context c, String auth, int userId, boolean stable) {
        final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);//1
        if (provider != null) {
            return provider;
        }
        IActivityManager.ContentProviderHolder holder = null;
        try {
            holder = ActivityManagerNative.getDefault().getContentProvider(
                    getApplicationThread(), auth, userId, stable);//2
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
        if (holder == null) {
            Slog.e(TAG, "Failed to find provider info for " + auth);
            return null;
        }
        holder = installProvider(c, holder, holder.info,
                true /*noisy*/, holder.noReleaseNeeded, stable);//3
        return holder.provider;
    }

注釋1處檢查ActivityThread中的ArrayMap類型的mProviderMap中是否有目標(biāo)ContentProvider存在质况,有則返回,沒有就會(huì)在注釋2處調(diào)用AMP的getContentProvider方法婆翔,最終會(huì)調(diào)用AMS的getContentProvider方法拯杠。注釋3處的installProvider方法用來將注釋2處返回的ContentProvider相關(guān)的數(shù)據(jù)存儲(chǔ)在mProviderMap中,起到緩存的作用啃奴,這樣使用相同的Content Provider時(shí),就不需要每次都要調(diào)用AMS的getContentProvider方法雄妥。使用我們接著查看AMS的getContentProvider方法最蕾,代碼如下所示。
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

    @Override
    public final ContentProviderHolder getContentProvider(
            IApplicationThread caller, String name, int userId, boolean stable) {
     ...
        return getContentProviderImpl(caller, name, null, stable, userId);
    }

getContentProvider方法return了getContentProviderImpl方法:
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

   private ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
            String name, IBinder token, boolean stable, int userId) {
       ...
       ProcessRecord proc = getProcessRecordLocked(
                                cpi.processName, cpr.appInfo.uid, false);//1
                        if (proc != null && proc.thread != null && !proc.killed) {
                            ...
                            if (!proc.pubProviders.containsKey(cpi.name)) {
                                checkTime(startTime, "getContentProviderImpl: scheduling install");
                                proc.pubProviders.put(cpi.name, cpr);
                                try {
                                    proc.thread.scheduleInstallProvider(cpi);//2
                                } catch (RemoteException e) {
                                }
                            }
                        } else {
                            checkTime(startTime, "getContentProviderImpl: before start process");
                            proc = startProcessLocked(cpi.processName,
                                    cpr.appInfo, false, 0, "content provider",
                                    new ComponentName(cpi.applicationInfo.packageName,
                                            cpi.name), false, false, false);//3
                            checkTime(startTime, "getContentProviderImpl: after start process");
                          ...
                        }
             ...           
                        
}

getContentProviderImpl方法的代碼很多老厌,這里截取了關(guān)鍵的部分瘟则。注釋1處通過getProcessRecordLocked方法來獲取目標(biāo)ContentProvider的應(yīng)用程序進(jìn)程信息,這些信息用ProcessRecord類型的proc來表示枝秤,如果該應(yīng)用進(jìn)程已經(jīng)啟動(dòng)就會(huì)調(diào)用注釋2處的代碼醋拧,否則就會(huì)調(diào)用注釋3的startProcessLocked方法來啟動(dòng)進(jìn)程。這里我們假設(shè)ContentProvider的應(yīng)用進(jìn)程還沒有啟動(dòng),關(guān)于應(yīng)用進(jìn)程啟動(dòng)過程丹壕,我在Android應(yīng)用程序進(jìn)程啟動(dòng)過程(前篇)已經(jīng)講過庆械,最終會(huì)調(diào)用ActivityThread的main方法,代碼如下所示菌赖。
frameworks/base/core/java/android/app/ActivityThread.java

 public static void main(String[] args) {
      ...
        Looper.prepareMainLooper();//1
        ActivityThread thread = new ActivityThread();//2
        thread.attach(false);
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();//3
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

注釋1處通過prepareMainLooper方法在ThreadLocal中獲取Looper缭乘,并在注釋3處開啟消息循環(huán)。在注釋2處創(chuàng)建了ActivityThread并調(diào)用了它的attach方法:
frameworks/base/core/java/android/app/ActivityThread.java

  private void attach(boolean system) {
  ...
    final IActivityManager mgr = ActivityManagerNative.getDefault();//1
            try {
                mgr.attachApplication(mAppThread);//2
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
  ...          
}

注釋1處最終會(huì)得到AMS琉用,在注釋2處調(diào)用AMS的attachApplication方法堕绩,并將ApplicationThread類型的mAppThread對(duì)象傳進(jìn)去。
query方法到AMS的調(diào)用過程邑时,如下面時(shí)序圖所示(省略應(yīng)用程序進(jìn)程啟動(dòng)過程)奴紧。

2.AMS啟動(dòng)Content Provider的過程

我們接著來查看AMS的attachApplication方法,如下所示晶丘。
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

@Override
public final void attachApplication(IApplicationThread thread) {
    synchronized (this) {
        int callingPid = Binder.getCallingPid();
        final long origId = Binder.clearCallingIdentity();
        attachApplicationLocked(thread, callingPid);
        Binder.restoreCallingIdentity(origId);
    }
}

attachApplication方法中又調(diào)用了attachApplicationLocked方法:
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

   private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {
   ...
   thread.bindApplication(processName, appInfo, providers, app.instrumentationClass,
                    profilerInfo, app.instrumentationArguments, app.instrumentationWatcher,
                    app.instrumentationUiAutomationConnection, testMode,
                    mBinderTransactionTrackingEnabled, enableTrackAllocation,
                    isRestrictedBackupMode || !normalMode, app.persistent,
                    new Configuration(mConfiguration), app.compat,
                    getCommonServicesLocked(app.isolated),
                    mCoreSettingsObserver.getCoreSettingsLocked());
...
}

attachApplicationLocked方法中調(diào)用了thread的bindApplication方法黍氮,thread是IApplicationThread類型的,從類型名字就可以看出來是用于進(jìn)程間通信铣口,這里實(shí)現(xiàn)bindApplication方法的是ApplicationThreadProxy類滤钱,它實(shí)現(xiàn)了IApplicationThread接口。
frameworks/base/core/java/android/app/ApplicationThreadNative.java

class ApplicationThreadProxy implements IApplicationThread {
...
    @Override
    public final void bindApplication(String packageName, ApplicationInfo info,
            List<ProviderInfo> providers, ComponentName testName, ProfilerInfo profilerInfo,
            Bundle testArgs, IInstrumentationWatcher testWatcher,
            IUiAutomationConnection uiAutomationConnection, int debugMode,
            boolean enableBinderTracking, boolean trackAllocation, boolean restrictedBackupMode,
            boolean persistent, Configuration config, CompatibilityInfo compatInfo,
            Map<String, IBinder> services, Bundle coreSettings) throws RemoteException {
      ...
        mRemote.transact(BIND_APPLICATION_TRANSACTION, data, null,
                IBinder.FLAG_ONEWAY);
        data.recycle();
    }
...
}

到目前為止脑题,上面的調(diào)用過程還是在AMS進(jìn)程中執(zhí)行的件缸,因此,需要通過IBinder類型的mRemote對(duì)象向新創(chuàng)建的應(yīng)用程序進(jìn)程(目標(biāo)Content Provider所在的進(jìn)程)發(fā)送BIND_APPLICATION_TRANSACTION類型的通信請(qǐng)求叔遂。處理這個(gè)通信請(qǐng)求的是在新創(chuàng)建的應(yīng)用程序進(jìn)程中執(zhí)行的ApplicationThread的bindApplication方法他炊,如下所示。
frameworks/base/core/java/android/app/ActivityThread.java

 public final void bindApplication(String processName, ApplicationInfo appInfo,
                List<ProviderInfo> providers, ComponentName instrumentationName,
                ProfilerInfo profilerInfo, Bundle instrumentationArgs,
                IInstrumentationWatcher instrumentationWatcher,
                IUiAutomationConnection instrumentationUiConnection, int debugMode,
                boolean enableBinderTracking, boolean trackAllocation,
                boolean isRestrictedBackupMode, boolean persistent, Configuration config,
                CompatibilityInfo compatInfo, Map<String, IBinder> services, Bundle coreSettings) {
                ...
                sendMessage(H.BIND_APPLICATION, data);
        }

調(diào)用sendMessage方法像H發(fā)送BIND_APPLICATION類型消息已艰,H的handleMessage方法如下所示痊末。
frameworks/base/core/java/android/app/ActivityThread.java

   public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
            switch (msg.what) {
            ...
            case BIND_APPLICATION:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
                    AppBindData data = (AppBindData)msg.obj;
                    handleBindApplication(data);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
  ...
  }
  ... 
}

我們接著查看handleBindApplication方法:
frameworks/base/core/java/android/app/ActivityThread.java

  private void handleBindApplication(AppBindData data) {
   ...
        final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);//1
         try {
                final ClassLoader cl = instrContext.getClassLoader();
                mInstrumentation = (Instrumentation)
                    cl.loadClass(data.instrumentationName.getClassName()).newInstance();//2
            } catch (Exception e) {
             ...
            }
            final ComponentName component = new ComponentName(ii.packageName, ii.name);
            mInstrumentation.init(this, instrContext, appContext, component,
                    data.instrumentationWatcher, data.instrumentationUiAutomationConnection);//3
           ...
            Application app = data.info.makeApplication(data.restrictedBackupMode, null);//4
            mInitialApplication = app;
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);//5
                    mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
                }
            }
          ...
           mInstrumentation.callApplicationOnCreate(app);//6
          ... 
  }

handleBindApplication方法的代碼很長(zhǎng),這里截取了主要的部分哩掺。注釋1處創(chuàng)建了ContextImpl 凿叠。注釋2處通過反射創(chuàng)建Instrumentation并在注釋3處初始化Instrumentation。注釋4處創(chuàng)建Application并且在注釋6處調(diào)用Application的onCreate方法嚼吞,這意味著Content Provider所在的應(yīng)用程序進(jìn)程已經(jīng)啟動(dòng)完畢盒件,在這之前,注釋5處調(diào)用installContentProviders方法來啟動(dòng)Content Provider舱禽,代碼如下所示炒刁。
frameworks/base/core/java/android/app/ActivityThread.java

private void installContentProviders(
        Context context, List<ProviderInfo> providers) {
    final ArrayList<IActivityManager.ContentProviderHolder> results =
        new ArrayList<IActivityManager.ContentProviderHolder>();

    for (ProviderInfo cpi : providers) {//1
        ...
        IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi,
                false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);//2
      ...
    }

    try {
        ActivityManagerNative.getDefault().publishContentProviders(
            getApplicationThread(), results);//3
    } catch (RemoteException ex) {
        throw ex.rethrowFromSystemServer();
    }
}

注釋1處遍歷當(dāng)前應(yīng)用程序進(jìn)程的ProviderInfo列表,得到每個(gè)Content Provider的ProviderInfo(存儲(chǔ)Content Provider的信息)誊稚,并在注釋2處調(diào)用installProvider方法來啟動(dòng)這些Content Provider翔始。在注釋3處通過AMS的publishContentProviders方法將這些Content Provider存儲(chǔ)在AMS的mProviderMap中罗心,這個(gè)mProviderMap在前面提到過,起到緩存的作用城瞎,防止每次使用相同的Content Provider時(shí)都會(huì)調(diào)用AMS的getContentProvider方法渤闷。來查看installProvider方法時(shí)如何啟動(dòng)Content Provider的,installProvider方法如下所示全谤。
frameworks/base/core/java/android/app/ActivityThread.java

 private IActivityManager.ContentProviderHolder installProvider(Context context,
            IActivityManager.ContentProviderHolder holder, ProviderInfo info,
            boolean noisy, boolean noReleaseNeeded, boolean stable) {
        ContentProvider localProvider = null;
   ...
                final java.lang.ClassLoader cl = c.getClassLoader();
                localProvider = (ContentProvider)cl.
                    loadClass(info.name).newInstance();//1
                provider = localProvider.getIContentProvider();
                if (provider == null) {
                  ...
                    return null;
                }
                if (DEBUG_PROVIDER) Slog.v(
                    TAG, "Instantiating local provider " + info.name);
                localProvider.attachInfo(c, info);//2
            } catch (java.lang.Exception e) {
               ...
                }
                return null;
            }
        }
           ...
        return retHolder;
    }

在注釋1處通過反射來創(chuàng)建ContentProvider類型的localProvider對(duì)象肤晓,并在注釋2處調(diào)用了它的attachInfo方法:
frameworks/base/core/java/android/content/ContentProvider.java

  private void attachInfo(Context context, ProviderInfo info, boolean testing) {
       ...
            ContentProvider.this.onCreate();
        }
    }

在attachInfo方法中調(diào)用了onCreate方法,它是一個(gè)抽象方法认然。這樣Content Provider就啟動(dòng)完畢补憾。
最后給出AMS啟動(dòng)Content Provider的時(shí)序圖。


歡迎關(guān)注我的微信公眾號(hào)卷员,第一時(shí)間獲得博客更新提醒盈匾,以及更多成體系的Android相關(guān)原創(chuàng)技術(shù)干貨。
掃一掃下方二維碼或者長(zhǎng)按識(shí)別二維碼毕骡,即可關(guān)注削饵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市未巫,隨后出現(xiàn)的幾起案子窿撬,更是在濱河造成了極大的恐慌,老刑警劉巖叙凡,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劈伴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡握爷,警方通過查閱死者的電腦和手機(jī)跛璧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來新啼,“玉大人追城,你說我怎么就攤上這事≡镒玻” “怎么了座柱?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)物舒。 經(jīng)常有香客問我辆布,道長(zhǎng),這世上最難降的妖魔是什么茶鉴? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮景用,結(jié)果婚禮上涵叮,老公的妹妹穿的比我還像新娘惭蹂。我一直安慰自己,他們只是感情好割粮,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布盾碗。 她就那樣靜靜地躺著,像睡著了一般舀瓢。 火紅的嫁衣襯著肌膚如雪廷雅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天京髓,我揣著相機(jī)與錄音航缀,去河邊找鬼。 笑死堰怨,一個(gè)胖子當(dāng)著我的面吹牛芥玉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播备图,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼灿巧,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了揽涮?” 一聲冷哼從身側(cè)響起抠藕,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蒋困,沒想到半個(gè)月后盾似,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡家破,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年颜说,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汰聋。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡门粪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出烹困,到底是詐尸還是另有隱情玄妈,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布髓梅,位于F島的核電站拟蜻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏枯饿。R本人自食惡果不足惜酝锅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奢方。 院中可真熱鬧搔扁,春花似錦爸舒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至苛聘,卻和暖如春涂炎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背设哗。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工唱捣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人熬拒。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓爷光,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親澎粟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蛀序,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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