安装流程

背景

安装影响的目录

  • /system/app
  • /data/app
  • /data/data
  • /data/dalvik-cache
  • /data/system
  • /data/system/package.xml 和 /data/system/package.list

安装的种类

package-install.png

安装流程

零、PackageInstaller

系统App的代码目录

  • package/apps/
  • framework/base/packages

1)android 7.0 PackageInstallerActivity

private void processPackageUri(final Uri packageUri) {
    mPackageURI = packageUri;

    final String scheme = packageUri.getScheme();
    final PackageUtil.AppSnippet as;

    switch (scheme) {
        case SCHEME_PACKAGE: {
            try {
                mPkgInfo = mPm.getPackageInfo(packageUri.getSchemeSpecificPart(),
                        PackageManager.GET_PERMISSIONS
                                | PackageManager.GET_UNINSTALLED_PACKAGES);
            } catch (NameNotFoundException e) {
            }
            if (mPkgInfo == null) {
                Log.w(TAG, "Requested package " + packageUri.getScheme()
                        + " not available. Discontinuing installation");
                showDialogInner(DLG_PACKAGE_ERROR);
                setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
                return;
            }
            as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo),
                    mPm.getApplicationIcon(mPkgInfo.applicationInfo));
        } break;

        case SCHEME_FILE: {
            File sourceFile = new File(packageUri.getPath());
            PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);

            // Check for parse errors
            if (parsed == null) {
                Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
                showDialogInner(DLG_PACKAGE_ERROR);
                setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
                return;
            }
            mPkgInfo = PackageParser.generatePackageInfo(parsed, null,
                    PackageManager.GET_PERMISSIONS, 0, 0, null,
                    new PackageUserState());
            as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
        } break;

        case SCHEME_CONTENT: {
            mStagingAsynTask = new StagingAsyncTask();
            mStagingAsynTask.execute(packageUri);
            return;
        }

        default: {
            Log.w(TAG, "Unsupported scheme " + scheme);
            setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
            clearCachedApkIfNeededAndFinish();
            return;
        }
    }

    PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);

    initiateInstall();
}

APP会执行拷贝,保存在/data/data/com.android.packageinstall/cache/

2)android 8.0 InstallStart.java

if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
    nextActivity.setClass(this, PackageInstallerActivity.class);
} else {
    Uri packageUri = intent.getData();

    if (packageUri == null) {
        // if there's nothing to do, quietly slip into the ether
        Intent result = new Intent();
        result.putExtra(Intent.EXTRA_INSTALL_RESULT,
                PackageManager.INSTALL_FAILED_INVALID_URI);
        setResult(RESULT_FIRST_USER, result);

        nextActivity = null;
    } else {
        if (packageUri.getScheme().equals(SCHEME_CONTENT)) {
            nextActivity.setClass(this, InstallStaging.class);
        } else {
            nextActivity.setClass(this, PackageInstallerActivity.class);
        }
    }
}

APP会执行拷贝,保存在/data/data/com.android.packageinstall/no_backup

检查
1、判断是否为未知来源,是否允许未知来源。
2、授予安装权限。
3、对安装包进行简单解析。
4、判断是否覆盖安装。
5、启动确认安装界面。

开始安装
7.0安装状态监听

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BROADCAST_ACTION);
registerReceiver(
        mBroadcastReceiver, intentFilter, BROADCAST_SENDER_PERMISSION, null /*scheduler*/);

private static final String BROADCAST_ACTION =
        "com.android.packageinstaller.ACTION_INSTALL_COMMIT";
private static final String BROADCAST_SENDER_PERMISSION =
        "android.permission.INSTALL_PACKAGES";

<!-- @SystemApi Allows an application to install packages.
<p>Not for use by third-party applications. -->
<permission android:name="android.permission.INSTALL_PACKAGES"
    android:protectionLevel="signature|privileged" />

8.0状态监听
在原有的基础上增加了
getPackageManager().getPackageInstaller().registerSessionCallback(mSessionCallback);
getPackageManager().getPackageInstaller().unregisterSessionCallback(mSessionCallback);
用来显示安装进度

PackageInstallerSession
通过Session与PackageManager建立起通信,并调用了installStage方法。

一、PackageManagerService.installStage

二、Handler.INIT_COPY

InstallParams
绑定ContainerService
frameworks/base/packages/DefaultContainerService
解析apk,获取APK安装大小。

Service that offers to inspect and copy files that may reside on removable
storage. This is designed to prevent the system process from holding onto
open files that cause the kernel to kill it when the underlying device is
 removed.

三、Handler.MCS_BOUND

InstallParams.startCopy
继承于HandlerParams

四、InstallParams.handleStartCopy()

1、PackageParser.parsePackageLite 轻量级解析
包名、版本号、单一的apk、唯一的拆分名称、签名信息(需要添加特定的flag)
装应用的期望位置installLocation,新版本是不起作用的。
android:installLocation=["auto" | "internalOnly" | "preferExternal"] >

2、getMinimalPackageInfo
获取PackageInfoLite。
1)PackageHelper.resolveInstallLocation
PackageInfoLite.recommendedInstallLocationAPK的安装位置,之后的安装流程,也在不停的对安装位置进行修正。如果解析失败,后边会尝试一次空间释放,重新解析。installLocation是

  • PackageHelper.RECOMMEND_FAILED_INVALID_URI
  • PackageHelper.RECOMMEND_FAILED_INVALID_APK
  • RECOMMEND_INSTALL_INTERNAL
    installFlags == PackageManager.INSTALL_INSTANT_APP
    installFlags == PackageManager.INSTALL_INTERNAL
    installLocation == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY
    installLocation == PackageInfo.INSTALL_LOCATION_AUTO
    若为auto,并且已经安装过,则选择之前的安装目录,若没有则选择内部。
  • RECOMMEND_INSTALL_EXTERNAL
    installFlags=PackageManager.INSTALL_EXTERNAL
    installLocation=PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL
  • RECOMMEND_FAILED_INSUFFICIENT_STORAGE
    如果什么都不是则返回此值。

3、计算安装空间
APK大小加上SO文件大小。

public static long calculateInstalledSize (PackageLite pkg, NativeLibraryHelper.Handle
    handle, boolean isForwardLocked, String abiOverride) throws IOException {
    long sizeBytes = 0;

    // Include raw APKs, and possibly unpacked resources
    for (String codePath : pkg.getAllCodePaths()) {
        final File codeFile = new File(codePath);
        sizeBytes += codeFile.length();

        if (isForwardLocked) {
            sizeBytes += PackageHelper.extractPublicFiles(codeFile, null);
        }
    }

    // Include all relevant native code
    sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);

    return sizeBytes;
}

public List<String> getAllCodePaths () {
    ArrayList<String> paths = new ArrayList<>();
    paths.add(baseCodePath);
    if (!ArrayUtils.isEmpty(splitCodePaths)) {
        Collections.addAll(paths, splitCodePaths);
    }
    return paths;
}

final long lowThreshold = storage.getStorageLowBytes(Environment.getDataDirectory());
mInstaller.freeCache(null, sizeBytes + lowThreshold, 0, 0);

private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500 * MB_IN_BYTES;
public long getStorageLowBytes(File path) {
    final long lowPercent = Settings.Global.getInt(mResolver,
            Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
    final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;

    final long maxLowBytes = Settings.Global.getLong(mResolver,
            Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);

    return Math.min(lowBytes, maxLowBytes);
}

4、installLocationPolicy
1)检查APK降级安装,只有当系统或者apk处于debug状态才允许。
2)对安装位置做修正。

5、InstallArgs
创建InstallArgs,分为三种

  • FileInstallArgs
  • AsecInstallArgs
  • MoveInstallArgs。

6、判断是否需要包验证
Intent.ACTION_PACKAGE_NEEDS_VERIFICATION

7、FileInstallArgs.copyApk
拷贝文件与so

private File buildStageDir(String volumeUuid, int sessionId, boolean isEphemeral) {
    final File stagingDir = buildStagingDir(volumeUuid, isEphemeral);
    return new File(stagingDir, "vmdl" + sessionId + ".tmp");
}

//--------------
final IParcelFileDescriptorFactory target = new IParcelFileDescriptorFactory.Stub() {
        @Override
        public ParcelFileDescriptor open(String name, int mode) throws RemoteException {
            if (!FileUtils.isValidExtFilename(name)) {
                throw new IllegalArgumentException("Invalid filename: " + name);
            }
            try {
                final File file = new File(codeFile, name);
                final FileDescriptor fd = Os.open(file.getAbsolutePath(),
                        O_RDWR | O_CREAT, 0644);
                Os.chmod(file.getAbsolutePath(), 0644);
                return new ParcelFileDescriptor(fd);
            } catch (ErrnoException e) {
                throw new RemoteException("Failed to open: " + e.getMessage());
            }
        }
    };

    int ret = PackageManager.INSTALL_SUCCEEDED;
    ret = imcs.copyPackage(origin.file.getAbsolutePath(), target);
    if (ret != PackageManager.INSTALL_SUCCEEDED) {
        Slog.e(TAG, "Failed to copy package");
        return ret;
    }

    final File libraryRoot = new File(codeFile, LIB_DIR_NAME);
    NativeLibraryHelper.Handle handle = null;
    try {
        handle = NativeLibraryHelper.Handle.create(codeFile);
        ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
                abiOverride);
    } catch (IOException e) {
        Slog.e(TAG, "Copying native libraries failed", e);
        ret = PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
    } finally {
        IoUtils.closeQuietly(handle);
    }

    return ret;
}

五、handleReturnCode(processPendingInstall)

  • installPackageLI
  • 备份部分。安装成功,需要备份服务。这里是在安装中进行数据恢复。
    FLAG_ALLOW_BACKUP,在manifest配置android:allowBackup="true"后。就可以使用adb对应用数据进行备份和还原。
    adb backup -f back.ab -noapk com.demo.allowbackup
    adb restore back.ab
  • 安装结束

六、installPackageLI

1)解析APK
PackageParser.parsePackage

PackageParser pp = new PackageParser();
pp.setSeparateProcesses(mSeparateProcesses);
pp.setDisplayMetrics(mMetrics);
final PackageParser.Package pkg;
try {
    pkg = pp.parsePackage(tmpPackageFile, parseFlags);
} catch (PackageParserException e) {
    res.setError("Failed parse during installPackageLI", e);
    return;
}
//-------------------
String separateProcesses = SystemProperties.get("debug.separate_processes");
if (separateProcesses != null && separateProcesses.length() > 0) {
    if ("*".equals(separateProcesses)) {
        mDefParseFlags = PackageParser.PARSE_IGNORE_PROCESSES;
        mSeparateProcesses = null;
        Slog.w(TAG, "Running with debug.separate_processes: * (ALL)");
    } else {
        mDefParseFlags = 0;
        mSeparateProcesses = separateProcesses.split(",");
        Slog.w(TAG, "Running with debug.separate_processes: "
                + separateProcesses);
    }
} else {
    mDefParseFlags = 0;
    mSeparateProcesses = null;
}

2)签名信息验证
PackageParser.collectCertificates
并对V1与V2签名进行验证。

try {
    // either use what we've been given or parse directly from the APK
    if (args.certificates != null) {
        try {
            PackageParser.populateCertificates(pkg, args.certificates);
        } catch (PackageParserException e) {
            // there was something wrong with the certificates we were given;
            // try to pull them from the APK
            PackageParser.collectCertificates(pkg, parseFlags);
        }
    } else {
        PackageParser.collectCertificates(pkg, parseFlags);
    }
} catch (PackageParserException e) {
    res.setError("Failed collect during installPackageLI", e);
    return;
}

3) 一些检查
对于覆盖安装

if (replace) {
        // Prevent apps opting out from runtime permissions
        PackageParser.Package oldPackage = mPackages.get(pkgName);
        final int oldTargetSdk = oldPackage.applicationInfo.targetSdkVersion;
        final int newTargetSdk = pkg.applicationInfo.targetSdkVersion;
        if (oldTargetSdk > Build.VERSION_CODES.LOLLIPOP_MR1
                && newTargetSdk <= Build.VERSION_CODES.LOLLIPOP_MR1) {
            res.setError(PackageManager.INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE,
                    "Package " + pkg.packageName + " new target SDK " + newTargetSdk
                            + " doesn't support runtime permissions but the old"
                            + " target SDK " + oldTargetSdk + " does.");
            return;
        }
        // Prevent apps from downgrading their targetSandbox.
        final int oldTargetSandbox = oldPackage.applicationInfo.targetSandboxVersion;
        final int newTargetSandbox = pkg.applicationInfo.targetSandboxVersion;
        if (oldTargetSandbox == 2 && newTargetSandbox != 2) {
            res.setError(PackageManager.INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE,
                    "Package " + pkg.packageName + " new target sandbox "
                    + newTargetSandbox + " is incompatible with the previous value of"
                    + oldTargetSandbox + ".");
            return;
        }

        // Prevent installing of child packages
        if (oldPackage.parentPackage != null) {
            res.setError(PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME,
                    "Package " + pkg.packageName + " is child of package "
                            + oldPackage.parentPackage + ". Child packages "
                            + "can be updated only through the parent package.");
            return;
        }
    }
}

//-------------------------------
The target sandbox for this app to use. The higher the sandbox version number, the higher the level of security. Its default value is `1`; you can also set it to `2`. Setting this attribute to `2` switches the app to a different SELinux sandbox.

The following restrictions apply to a level 2 sandbox:

*   The default value of [`usesCleartextTraffic`](https://developer.android.com/guide/topics/manifest/application-element.html#usesCleartextTraffic) in the Network Security Config is false.
*   Uid sharing is not permitted.

For Android Instant Apps targeting Android 8.0 (API level 26) or higher, this attribute must be set to 2\. You can set the sandbox level in the installed version of your app to the less restrictive level 1, but if you do so, your app does not persist app data from the instant app to the installed version of your app. You must set the installed app's sandbox value to 2 in order for the data to persist from the instant app to the installed version.

Once an app is installed, you can only update its target sandbox value to a higher value. To downgrade the target sandbox value, you must uninstall the app and replace it with a version whose manifest contains a lower value for this attribute.

//--------------------------------
// Quick sanity check that we're signed correctly if updating;
// we'll check this again later when scanning, but we want to
// bail early here before tripping over redefined permissions.
if (shouldCheckUpgradeKeySetLP(signatureCheckPs, scanFlags)) {
    if (!checkUpgradeKeySetLP(signatureCheckPs, pkg)) {
        res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "
                + pkg.packageName + " upgrade keys do not match the "
                + "previously installed version");
        return;
    }
} else {
    try {
        verifySignaturesLP(signatureCheckPs, pkg);
    } catch (PackageManagerException e) {
        res.setError(e.error, e.getMessage());
        return;
    }
}

检查APK中已经定义的权限是否已经被其他应用定义了。如果重定义的时系统的权限,则忽略这个权限。
如果是APP重复定义,则安装错误。

**4)拷贝so文件

try {
    derivePackageAbi(pkg, new File(pkg.codePath), args.abiOverride,
            true /* extract libs */);
} catch (PackageManagerException pme) {
    Slog.e(TAG, "Error deriving application ABI", pme);
    res.setError(INSTALL_FAILED_INTERNAL_ERROR, "Error deriving application ABI");
    return;
}

6)执行Dex优化

int result = mPackageDexOptimizer
        .performDexOpt(pkg, null /* instruction sets */, false /* forceDex */,
                false /* defer */, false /* inclDependencies */,
                true /* boot complete */);
if (result == PackageDexOptimizer.DEX_OPT_FAILED) {
    res.setError(INSTALL_FAILED_DEXOPT, "Dexopt failed for " + pkg.codePath);
    return;
}

7)重命名
if (!args.doRename(res.returnCode, pkg, oldCodePath)) {
// 如果重命名失败,则报错,退出,安装失败原因:无法重命名
res.setError(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed rename");
return;
}

8)IntentFilter

startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);

9)安装或者覆盖

// ******* 第八步 *******
if (replace) {
    replacePackageLI(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,
            installerPackageName, volumeUuid, res);
} else {
    installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
            args.user, installerPackageName, volumeUuid, res);
}

10)

synchronized (mPackages) {
    final PackageSetting ps = mSettings.mPackages.get(pkgName);
    if (ps != null) {
        res.newUsers = ps.queryInstalledUsers(sUserManager.getUserIds(), true);
    }
}

七、安装包信息扫描与设置

scanPackageLI
updateSettingsLI

八、POST_INSTALL

安装收尾,发送广播
ACTION_PACKAGE_ADDED
ACTION_PACKAGE_REPLACED
ACTION_MY_PACKAGE_REPLACED
告诉PackageInstaller安装结果

if (installObserver != null) {
    try {
        Bundle extras = extrasForInstallResult(res);
        installObserver.onPackageInstalled(res.name, res.returnCode,
                res.returnMsg, extras);
    } catch (RemoteException e) {
        Slog.i(TAG, "Observer no longer exists.");
    }
}

其他安装方式

pm install

private int doCreateSession (SessionParams params, String installerPackageName,int userId)
      throws RemoteException {
    userId = translateUserId(userId, "runInstallCreate");
    if (userId == UserHandle.USER_ALL) {
        userId = UserHandle.USER_SYSTEM;
        params.installFlags |= PackageManager.INSTALL_ALL_USERS;
    }

    final int sessionId = mInstaller.createSession(params, installerPackageName, userId);
    return sessionId;
}

Pm      : Error
Pm      : java.lang.SecurityException: Permission Denial: runInstallCreate from pm command asks to run as user -1 but is calling from user 0; this requires android.permission.INTERACT_ACROSS_USERS_FULL
Pm      :   at android.os.Parcel.readException(Parcel.java:1700)
Pm      :   at android.os.Parcel.readException(Parcel.java:1653)
Pm      :   at android.app.ActivityManagerProxy.handleIncomingUser(ActivityManagerNative.java:4880)
Pm      :   at android.app.ActivityManager.handleIncomingUser(ActivityManager.java:3430)
Pm      :   at com.android.commands.pm.Pm.translateUserId(Pm.java:345)
Pm      :   at com.android.commands.pm.Pm.doCreateSession(Pm.java:555)
Pm      :   at com.android.commands.pm.Pm.runInstall(Pm.java:401)
Pm      :   at com.android.commands.pm.Pm.run(Pm.java:151)
Pm      :   at com.android.commands.pm.Pm.main(Pm.java:108)
Pm      :   at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)
Pm      :   at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:364)
art     : System.exit called, status: 1


<!-- @SystemApi Fuller form of {@link android.Manifest.permission#INTERACT_ACROSS_USERS}
    that removes restrictions on where broadcasts can be sent and allows other
    types of interactions
    @hide -- >
<permission android:name = "android.permission.INTERACT_ACROSS_USERS_FULL"
    android:protectionLevel = "signature|installer" / >
企业微信截图_1552741257529.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容