Android插件化(二)

广播插件的两种实现模式

接上一篇插件化(一),已经实现了通过插装式实现activity插件和service插件,这两种的实现是一样的,但是广播就不同了,广播分为静态广播和动态广播,那么是怎么实现广播插件的运行呢。我们先从广播的两种注册方式以及使用开始分析。

静态广播和动态广播:

动态广播不需要再Manifest中声明

静态广播是需要的,声明之后通过apk的安装,系统解析manifest来实现广播的注册,从而可以接受到跨进程的消息

动态广播

动态广播其实和activity、service一样,也是实现一个接口

public interface ProxyBroadCastInterface {

    void attch(Context context);

    void onReceive(Context context, Intent intent);
}

我们插件中对业务进行处理的广播

public class MyReceive extends BroadcastReceiver implements ProxyBroadCastInterface {
    @Override
    public void attch(Context context) {
        // 广播绑定成功
        Toast.makeText(context,"广播绑定成功",Toast.LENGTH_LONG).show();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        //接受到广播
        Toast.makeText(context," 接受到广播",Toast.LENGTH_LONG).show();
    }
}

然后在我们的插件的mainActivity中加两个按钮 一个是注册广播,一个发送广播,

findViewById(R.id.mRegiestBroadCast).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                IntentFilter intent = new IntentFilter();
                intent.addAction("com.plugin.app.receive");
                //调用register方法 肯定要调用宿主的 所以重写baseactivity
                registerReceiver(new MyReceive(), intent);
            }
        });

        findViewById(R.id.mSendBroadCast).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setAction("com.plugin.app.receive");
                sendBroadcast(intent);
            }
        });

那么我们这里调用注册广播和发送广播的方法,就是调用的baseActivity ,那么我就重写BaseActivity的有关广播的两个方法

 @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        return that.registerReceiver(receiver, filter);
    }

    @Override
    public void sendBroadcast(Intent intent) {
        that.sendBroadcast(intent);
    }

所以最终都是调用我们宿主插件activity的两个方法,在重写插件的activity的两个方法

@Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        //收到调用注册动态广播的申请,那么我就帮你完成
        IntentFilter newIntentFilter = new IntentFilter();
        for (int i = 0; i < filter.countActions(); i++) {
            newIntentFilter.addAction(filter.getAction(i));
        }
        return super.registerReceiver(new ProxyReceive(receiver.getClass().getName(),this), newIntentFilter);
    }

下面是我们的代理的广播

public class ProxyReceive extends BroadcastReceiver {
    String className;
    private  ProxyBroadCastInterface receiveObj;

    public ProxyReceive(String className,Context context) {
        this.className = className;
        //这里通过classname 得到class对象,然后
        try {
            Class<?> receiverClass = HookManager.getInstance().getClassLoader().loadClass(className);
            Constructor constructorReceiver = receiverClass.getConstructor(new Class[]{});
            receiveObj = (ProxyBroadCastInterface) constructorReceiver.newInstance(new Object[]{});
            receiveObj.attch(context);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        receiveObj.onReceive(context, intent);
    }
}

上运行效果


plugin_receive.gif

上面完成对动态注册的广播插件的运行,下面就到静态注册了

静态注册广播

  1. 内存消耗高,因为常驻内存的。但是动态注册的广播是受activity的生命周期影响的

静态广播是没有跟随我们的app启动,而是手机启动的时候就已经被加载到内存中了,但是前提是你这个app已经安装到手机上了,所以想要实现我们的静态广播插件的实现,的对apk的安装进行分析了。

apk的安装原理

Android有四种安装方式
  1. 已安装的系统应用安装其他应用

    特点:没有安装界面,直接安装

  2. 手机应用市场安装apk

    特点:直接安装,没有安装界面

  3. ADB工具安装

    特点:无安装界面

  4. 第三方应用安装

    特点:有安装界面,是由PackageInstaller.apk应用 来处理安装及卸载过程的界面。

那么安装时,系统帮我们做了什么事,BroadCastReceive又是怎么注册的
  1. 安装时吧apk文件复制到data/app这个目录 (用户程序安装的目录) .
  2. 然后开辟存放应用数据的目录: /data/data/ 包名
  3. 将apk中的dex文件安装到data/dalvik-cache目录下(dex文件是dalvik虚拟机的可执行文件,其大小约为apk文件大小的四分之一)。

所以安装一个apk 系统帮我一共做了三件事,复制apk、开辟存放数据的目录、在吧dex文件进行拷贝。

真正加载广播,是在系统发生启动的时候,这个时候回将手机上所有的app都安装一遍。而我们的静态广播是注册在manifest文件中的,当我们的apk通过PMS安装的时候回去解析Manifest文件,将其中的四大组件拿出来进行注册。那我们的插件app是没有安装的,那么如何将它的清单文件中的广播拿出来呢,所以我们有必要来梳理一些apk的安装,也就是PMS。

PMS(PackageManagerService)服务

PMS服务是在开机的时候由SystemServer (系统服务去调用的)

SystemServer.java
//这是它的main方法
public static void main(String[] args) {
        new SystemServer().run();
}
private void run(){
  ...
  mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
  mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
                ...
 }

那么我们分析一下系统是怎么去扫描data/app下面的程序呢
先看这个PackageManagerService.java文件

//这个是main方法,当系统服务运行起来就会调用PMS的main方法
public static PackageManagerService main(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        // Self-check for initial settings.
        PackageManagerServiceCompilerMapping.checkProperties();
    
        //调用了构造方法
        PackageManagerService m = new PackageManagerService(context, installer,
                factoryTest, onlyCore);
        m.enableSystemUserPackages();
        ServiceManager.addService("package", m);
        final PackageManagerNative pmn = m.new PackageManagerNative();
        ServiceManager.addService("package_native", pmn);
        return m;
    }

可以看到main方法里面调用了构造方法 ,看看构造方法里面做了什么事

public PackageManagerService(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore){
    ...
    synchronized (mPackages){
        ...
            //这个dataDir目录就是/data 
            File dataDir = Environment.getDataDirectory();
            mAppInstallDir = new File(dataDir, "app");//这个目录就是我们第三方app程序的目录
            mAppLib32InstallDir = new File(dataDir, "app-lib");
            mAsecInternalPath = new File(dataDir, "app-asec").getPath();
            mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
        ...
            //下面会调用这个方法 通过方法名字,可以看出来 是扫描/data/app这个目录下的文件 将路径传进去
            scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
    }
}

那我们在分析扫描data/app 这个里面做了什么事呢

private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {
    final File[] files = dir.listFiles();//显示吧所有的文件全部拿到 盘算是不是为空 如果为空,直接返回
        if (ArrayUtils.isEmpty(files)) {
            Log.d(TAG, "No files in app dir " + dir);
            return;
        }
    //接着遍历data/app下面的目录
    for (File file : files) {
        //如果是一个apk文件  那这个isPackage就为true
        final boolean isPackage = (isApkFile(file) || file.isDirectory())
                    && !PackageInstallerService.isStageName(file.getName());
        ...
           
       parallelPackageParser.submit(file, parseFlags);
    }
}

从上面这个submit方法可以发现,是将当前一个apk文件穿进去了

接下来到这个ParallelPackageParser.java文件了,那么我们看它的submit方法

/**
@params scanFile  当前要解析的apk文件
*/
public void submit(File scanFile, int parseFlags) {
    ...
        //看到在解析这个apk文件时 new了一个PackageParse这个javabean
        PackageParser pp = new PackageParser();
        pp.setSeparateProcesses(mSeparateProcesses);
        pp.setOnlyCoreApps(mOnlyCore);
        pp.setDisplayMetrics(mMetrics);
        pp.setCacheDir(mCacheDir);
        pp.setCallback(mPackageParserCallback);
        pr.scanFile = scanFile;
        //调用了PackageParse里面的parsePackage方法,
        pr.pkg = parsePackage(pp, scanFile, parseFlags);
    ...
}

从上面看到 调用了这个PackageParse这个类,很重要 ,那么我们去看看parsePackage这个方法到是怎么去解析一个apk文件的。

当前跳到了 PackageParse.java 文件中

public Package parsePackage(File packageFile, int flags, boolean useCaches)
            throws PackageParserException {
    ...
        //packageFile 就是从PMS那边一步一步传过来要解析的当前的apk文件
        //最后调用了这个方法
         parsed = parseMonolithicPackage(packageFile, flags);
    ...
}

 public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
      final AssetManager assets = newConfiguredAssetManager();
     ...
         //调用这个方法 并且里面new了一个AssetManager 我们可以猜到里面有调用addAssetPath这个方法
         final Package pkg = parseBaseApk(apkFile, assets, flags);
         pkg.setCodePath(apkFile.getAbsolutePath());
         pkg.setUse32bitAbi(lite.use32bitAbi);
         return pkg;
     ...
 }

private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
            throws PackageParserException {
    ...
     res = new Resources(assets, mMetrics, null);
    //  private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
     parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
    final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);
    
}

从上面openXmlResourceParser 这个方法看出来 现在是解析Manifest.xml文件并且最终得到一个了一个XmlResourceParser对象,然后将这个对象传到parseBaseApk这个方法得到当前apk的封装对象PackageParse.package 它是PackageParse的内部类

可以看下类结构图:

PackageParse$Package.png

这个xml解析我跟到最后发现是一个native方法,所以也不必看了,直接看parseBaseApk这个方法

还是在PackageParser.java类里面

/**
@params apkPath 当前要解析的apk文件
@params res 是在上一步new出来的 
@params parser 是manifest的解析器
*/
private Package parseBaseApk(String apkPath, Resources res, XmlResourceParser parser, int flags,
            String[] outError) throws XmlPullParserException, IOException {
    ...
    //可以看到它将我们的pkgName和apkPath转成一个数组,然后添加到我们的res里面了
    String[] overlayPaths = mCallback.getOverlayPaths(pkgName, apkPath);
            if (overlayPaths != null && overlayPaths.length > 0) {
                for (String overlayPath : overlayPaths) {
                    res.getAssets().addOverlayPath(overlayPath);
                }
            }
    ...
    final Package pkg = new Package(pkgName);
    ...
    //又调用这个方法
    return parseBaseApkCommon(pkg, null, res, parser, flags, outError);
 }

一入源码深似海,反正都是各种调用,其实发现真正做事情就那么几个方法,只能时候封装的太好了,题外话。。

再看看parseBaseApkCommon 这个方法

private Package parseBaseApkCommon(Package pkg, Set<String> acceptedTags, Resources res,
XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException,
            IOException {
                ...
       //private static final String TAG_APPLICATION = "application";
       if (tagName.equals(TAG_APPLICATION)) {
           //从这里我们看出来 才TM是真正解析application标签了,前面分析了那么多,现在才是目的
           ...
              //又来一个调用的方法 只能进去看看了
            if (!parseBaseApplication(pkg, res, parser, flags, outError)) {
                    return null;
               }
       }
 }

// 从名字就能看出来 解析application标签的

private boolean parseBaseApplication(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError)
   ...
    while(...){
         String tagName = parser.getName();
        //得到我们当前解析的标签名
        if (tagName.equals("activity")) {
            //如果是activity 
             Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, false,
                        owner.baseHardwareAccelerated);
                if (a == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.activities.add(a);

            } else if (tagName.equals("receiver")) {
            // 如果是receiver
                Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs,
                        true, false);
                if (a == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.receivers.add(a);

            } else if (tagName.equals("service")) {
                Service s = parseService(owner, res, parser, flags, outError, cachedArgs);
                if (s == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.services.add(s);

            } else if (tagName.equals("provider")) {
                Provider p = parseProvider(owner, res, parser, flags, outError, cachedArgs);
                if (p == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.providers.add(p);
            }
    }
    
}

从上面我们可以看出来,通过xml解析将四大组件解析出来,放到Package这个类的四个集合中去,如下

PackageParse$Package

public final ArrayList<Activity> activities = new ArrayList<Activity>(0);
public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
public final ArrayList<Provider> providers = new ArrayList<Provider>(0);
public final ArrayList<Service> services = new ArrayList<Service>(0);

我们看到存放activity的集合的泛型的是Activity,这里要说明的是不是我们四大组件的activity,而是我们的PackageParse的内部类 Activity(PackageParse$Package)。并且activity和receiver集合的泛型都是一样的,那么这是为啥?因为我们在清单文件 里面注册activity或者receiver都要声明IntetFilter,所以在设计的时候,能复用就复用。对于Service和Provider就不一样了。

那么我们就看看是如何是怎样将标签变成一个Activity的,看parseActivity方法

private Activity parseActivity(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError, CachedComponentArgs cachedArgs,
            boolean receiver, boolean hardwareAccelerated)
            throws XmlPullParserException, IOException {
    
    ...
        //如果是reveicer就是true 否则就是false
   cachedArgs.mActivityArgs.tag = receiver ? "<receiver>" : "<activity>";
    if (!receiver) {
        //如果是activity 
    }else{
        // 如果是receiver
       
    }
    ...
        //开始解析activity和receiver的intent-Filter
       while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
               && (type != XmlPullParser.END_TAG
                       || parser.getDepth() > outerDepth)) {
           if (parser.getName().equals("intent-filter")) {
               //可以看到这里就解析的intent-filter
               ActivityIntentInfo intent = new ActivityIntentInfo(a);
               ...
                //这个a 就是我们的Activity,intents就是用来装Intent的一个集合,这里是ActivityIntenrInfo,那么ActivityIntentInfo是继承IntentInfo,IntentInfo又是继承自IntentFilter的
                a.intents.add(intent);
           }
       }
}

那么是在哪里解析activity和receiver的name的呢,那一个标签肯定有名字的对吧,通过查找,发现名字是在一个

Activity类里面的ActivityInfo里面 而且ActivityInfo继承ComponentInfo 继承PackageItemInfo,最终发现在PackageItemInfo这个类里面,看源码的注释可以看到

public class PackageItemInfo {
 /**
     * Public name of this item. From the "android:name" attribute.
     */
    public String name;// android:name 就是我们要找的组件的名字
}

而且这个ActivityInfo 是通过调用这个方法生成的

public static final ActivityInfo generateActivityInfo(ActivityInfo ai, int flags,
            PackageUserState state, int userId) {
        if (ai == null) return null;
        if (!checkUseInstalledOrHidden(flags, state, ai.applicationInfo)) {
            return null;
        }
        // This is only used to return the ResolverActivity; we will just always
        // make a copy.
        ai = new ActivityInfo(ai);
        ai.applicationInfo = generateApplicationInfo(ai.applicationInfo, flags, state, userId);
        return ai;
    }

至此分析的差不多了 ,也知道开启启动,Android为啥启动的这么慢,就是因为要遍历所有的apk,安装一遍,解析MainFest文件这些操作。

插件中静态广播的解析

通过上面的分析,知道广播是怎样解析的,怎样加载到内存中去的,通过什么样的方式,那些方法,怎么调用的,最终又是封装在那些类里面,知道了这些原理,我们才可以去加载插件apk中的一个静态广播,其实上面分析了这么多,就是为了干这个事情,也顺带着把我们的PMS稍微分析了一遍

那么我们就开始解析我们插件中的广播,在HookManager 的loadPathToPlugin这个方法中解析我们的清单文件

/**
     * 通过解析清单文件来 拿到静态广播并且进行注册
     *
     * @param activity
     * @param path
     */
    private void parseReceivers(Activity activity, String path) {
        try {
            //我们知道解析一个apk文件的入口就是PackageParse.parsePackage 这个方法
            //所以我们使用反射 来调用这个方法 最终得到了一个 PackageParse$Package 这个类
            Class<?> mPackageParseClass = Class.forName("android.content.pm.PackageParser");
            Method mParsePackageMethod = mPackageParseClass.getDeclaredMethod("parsePackage", File.class, int.class);
            Object mPackageParseObj = mPackageParseClass.newInstance();
            Object mPackageObj = mParsePackageMethod.invoke(mPackageParseObj, new File(path), PackageManager.GET_ACTIVITIES);

            //解析出来的receiver就存在PackageParse$Package 这个类里面的一个receivers集合里面
            Field mReceiversListField = mPackageObj.getClass().getDeclaredField("receivers");
            //然后得到反射得到这个属性的值 最终得到一个集合
            List mReceiverList = (List) mReceiversListField.get(mPackageObj);

            //接下来我们要拿到 IntentFilter 和name属性 这样才能反射创建对象,动态在宿主里面注册广播
            Class<?> mComponetClass = Class.forName("android.content.pm.PackageParser$Component");
            Field mIntentFields = mComponetClass.getDeclaredField("intents");

            //这两行是为了调用generateActivityInfo 而反射拿到的参数
            Class<?> mPackageParse$ActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
            Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");


            Object mPackzgeUserStateObj = mPackageUserStateClass.newInstance();


            // 拿到generateActivityInfo这个方法
            Method mGeneReceiverInfo = mPackageParseClass.getMethod("generateActivityInfo", mPackageParse$ActivityClass, int.class, mPackageUserStateClass, int.class);

            Class<?> mUserHandlerClass = Class.forName("android.os.UserHandle");
            Method getCallingUserIdMethod = mUserHandlerClass.getDeclaredMethod("getCallingUserId");

            int userId = (int) getCallingUserIdMethod.invoke(null);


            //然后for循环 去拿到name和 intentFilter
            for (Object activityObj : mReceiverList) {
                //调用generateActivityInfo
                // 这个是我们要调用的方法的形参 public static final ActivityInfo generateActivityInfo(Activity a, int flags,PackageUserState state, int userId);
                //得到一个ActivityInfo
                ActivityInfo info = (ActivityInfo) mGeneReceiverInfo.invoke(mPackageParseObj, activityObj, 0, mPackzgeUserStateObj, userId);
                //拿到这个name 相当于我们在清单文件中Android:name 这样,是一个全类名,然后通过反射去创建对象
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) getClassLoader().loadClass(info.name).newInstance();

                //在拿到IntentFilter
                List<? extends IntentFilter> intents = (List<? extends IntentFilter>) mIntentFields.get(activityObj);
                //然后直接调用registerReceiver方法发
                for (IntentFilter intentFilter : intents) {
                    activity.registerReceiver(broadcastReceiver, intentFilter);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

ok 然后创建广播和插件进行注册,下面是效果图

plugin_static_receive.gif

这是源码地址:https://github.com/doujd/PluginDemo1

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

推荐阅读更多精彩内容