Android App安装流程

1. PackageMS相关框架

PackageMS相关框架

2.1 调用PackageMS解析

根据Intent找到apk路径,调用PackageUtil.getPackageInfo 解析
PackageInstallerActivity.onCreate
    PackageUtil.getPackageInfo(sourceFile)
        parser.parseMonolithicPackage
            parseBaseApk // 解析apk AndroidManifest.xml
    PackageParse.generatePackageInfo  // 转化为PackageInfo 对象
    ...
    initiateInstall // 判断App是否已经安装过 ApplicationInfo.FLAG_INSTALLED
      startInstallConfirm // 对话框,是否安装及app获得的那些权限
    ...
    startInstall 
        startActivity(newIntent) // 传递给PackageMS

2.2 PackageInstallerActivity::onClick

public void onClick(View v) {
    if (v == mOk) {
        if (mOkCanInstall || mScrollView == null) {
            if (mSessionId != -1) {
                mInstaller.setPermissionsResult(mSessionId, true);
                clearCachedApkIfNeededAndFinish();
            } else {
                startInstall(); // 2.3
            }
        } else {
            mScrollView.pageScroll(View.FOCUS_DOWN);
        }
    } else if (v == mCancel) {
        // Cancel and finish
        setResult(RESULT_CANCELED);
        if (mSessionId != -1) {
            mInstaller.setPermissionsResult(mSessionId, false);
        }
        clearCachedApkIfNeededAndFinish();
    }
}

2.3 PackageInstallerActivity::startInstall

private void startInstall() {
    Intent newIntent = new Intent();
    newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
            mPkgInfo.applicationInfo);
    newIntent.setData(mPackageURI);
    newIntent.setClass(this, InstallAppProgress.class);
    ...
    startActivity(newIntent);
    finish();
}

2.4 WearPackageInstallerService::installPackage

public int onStartCommand(Intent intent, int flags, int startId) {
    ...
    WearPackageArgs.setStartId(intentBundle, startId);
    WearPackageArgs.setPackageName(intentBundle, packageName);
    if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) {
        Message msg = mServiceHandler.obtainMessage(START_INSTALL);
        msg.setData(intentBundle);
        mServiceHandler.sendMessage(msg);
    } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) {
        Message msg = mServiceHandler.obtainMessage(START_UNINSTALL);
        msg.setData(intentBundle);
        mServiceHandler.sendMessage(msg);
    }
    return START_NOT_STICKY;
}

2.5 WearPackageInstallerService::installPackage

private void installPackage(Bundle argsBundle) {
    // Finally install the package.
    pm.installPackage(Uri.fromFile(tempFile),
            new PackageInstallObserver(this, lock, startId, packageName),
                installFlags, packageName); // 3.1
}

3.1 PackageMS::installPackageAsUser

public void installPackageAsUser(...) {
    // 权限检查 
    ...
    final Message msg = mHandler.obtainMessage(INIT_COPY);
    final VerificationInfo verificationInfo = new VerificationInfo(
            null /*originatingUri*/, null /*referrer*/, -1 /*originatingUid*/, callingUid);
    final InstallParams params = new InstallParams(origin, null /*moveInfo*/, observer,
            installFlags, installerPackageName, null /*volumeUuid*/, verificationInfo, user,
            null /*packageAbiOverride*/, null /*grantedPermissions*/,
            null /*certificates*/);
    params.setTraceMethod("installAsUser").setTraceCookie(System.identityHashCode(params));
    msg.obj = params;

    mHandler.sendMessage(msg);// 3.2
}

3.1.2 权限检查逻辑

installPackageAsUser 权限检查相关

3.2 PackageMS::PackageHandler::doHandleMessage

void doHandleMessage(Message msg) {
    switch (msg.what) {
        case INIT_COPY: {
            HandlerParams params = (HandlerParams) msg.obj;
            int idx = mPendingInstalls.size();
            // If a bind was already initiated we dont really
            // need to do anything. The pending install
            // will be processed later on.
            if (!mBound) {
                // If this is the only one pending we might
                // have to bind to the service again.
                if (!connectToService()) {  // 3.3
                    params.serviceError();
                    return;
                } else {
                    // Once we bind to the service, the first
                    // pending request will be processed.
                    mPendingInstalls.add(idx, params); // 连接成功,则添加到mPendingInstalls
                }
            } else {
                mPendingInstalls.add(idx, params);
                // Already bound to the service. Just make
                // sure we trigger off processing the first request.
                if (idx == 0) {
                    mHandler.sendEmptyMessage(MCS_BOUND); // 3.7
                }
            }
            break;
        }
    }
}

3.3 PackageMS::PackageHandler::connectToService

static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName(DEFAULT_CONTAINER_PACKAGE,
    "com.android.defcontainer.DefaultContainerService");
final private DefaultContainerConnection mDefContainerConn = new DefaultContainerConnection(); // 3.4
private boolean connectToService() {
    Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
    Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
    if (mContext.bindServiceAsUser(service, mDefContainerConn,
            Context.BIND_AUTO_CREATE, UserHandle.SYSTEM)) {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        mBound = true;

        final long DEFCONTAINER_CHECK = 1 * 1000;
        final Message msg = mHandler.obtainMessage(MCS_CHECK);
        mHandler.sendMessageDelayed(msg, DEFCONTAINER_CHECK); // 3.5

        return true;
    }
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    return false;
}

3.4 PackageMS::DefaultContainerConnection

    class DefaultContainerConnection implements ServiceConnection {
        public void onServiceConnected(ComponentName name, IBinder service) {
            mServiceConnected = true;
            mServiceCheck = 2111;
            IMediaContainerService imcs =
                IMediaContainerService.Stub.asInterface(service); // 3.4.2
            mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs));
        }

        public void onServiceDisconnected(ComponentName name) {
            mServiceConnected = false;
        }
    }

3.4.2 DefaultContainerService::mBinder

private IMediaContainerService.Stub mBinder = new IMediaContainerService.Stub() {
        public int copyPackage(String packagePath, IParcelFileDescriptorFactory target) {
            if (packagePath == null || target == null) {
                return PackageManager.INSTALL_FAILED_INVALID_URI;
            }

            PackageLite pkg = null;
            try {
                final File packageFile = new File(packagePath);
                pkg = PackageParser.parsePackageLite(packageFile, 0);
                return copyPackageInner(pkg, target); // copy相关调用栈 DefaultContainerService::copyFile  --> FileUtils.copyFile
            } catch (PackageParserException | IOException | RemoteException e) {
                Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);
                return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
            }
        }
}

3.5 PackageMS::PackageHandler::doHandleMessage

void doHandleMessage(Message msg) {
    case MCS_CHECK: {
        mServiceCheck ++;
        if (!mServiceConnected && mServiceCheck <= 3) { // 共尝试连接4次
            connectToService();
        }
        break;
    }
}

3.6 INIT_COPY逻辑

INIT_COPY逻辑

3.7 PackageMS::PackageHandler::doHandleMessage

void doHandleMessage(Message msg) {
...
case MCS_BOUND: {
   if (msg.obj != null) {
        mContainerService = (IMediaContainerService) msg.obj;
    }
    if (mContainerService == null) {
        if (!mBound) {
            for (HandlerParams params : mPendingInstalls) {
                // Indicate service bind error
                params.serviceError(); // 未绑定处理
                return;
            }
            mPendingInstalls.clear();
        } 
    } else if (mPendingInstalls.size() > 0) {
        HandlerParams params = mPendingInstalls.get(0);
        if (params != null) { 
            boolean startCopy = params.startCopy(); // 3.8 
            if(startCopy) {
                if (mPendingInstalls.size() > 0) {
                    mPendingInstalls.remove(0);
                }
                if (mPendingInstalls.size() == 0) {
                    if (mBound) {
                        removeMessages(MCS_UNBIND);
                        Message ubmsg = obtainMessage(MCS_UNBIND); // 3.9
                        // Unbind after a little delay, to avoid
                        // continual thrashing.
                        sendMessageDelayed(ubmsg, 10000);
                    }
                } else {
                    mHandler.sendEmptyMessage(MCS_BOUND); // 3.10
                }
            }
        }
    }
}

3.8 PackageMS::HandlerParams::startCopy

final boolean startCopy() {
    boolean res;
    try {
        if (++++mRetries > MAX_RETRIES) {
            Slog.w(TAG, "Failed to invoke remote methods on default container service. Giving up");
            mHandler.sendEmptyMessage(MCS_GIVE_UP);
            handleServiceError();
            return false;
        } else {
            handleStartCopy(); // 3.11
            res = true;
        }
    } catch (RemoteException e) {
        if (DEBUG_INSTALL) Slog.i(TAG, "Posting install MCS_RECONNECT");
        mHandler.sendEmptyMessage(MCS_RECONNECT);
        res = false;
    }
    handleReturnCode(); // 3.12
    return res;
}

3.9 PackageMS::PackageHandler::doHandleMessage

case MCS_UNBIND: {
    if (mPendingInstalls.size() == 0 && mPendingVerification.size() == 0) {
        if (mBound) {
            if (DEBUG_INSTALL) Slog.i(TAG, "calling disconnectService()");

            disconnectService();
        }
    } else if (mPendingInstalls.size() > 0) {
        // There are more pending requests in queue.
        // Just post MCS_BOUND message to trigger processing
        // of next pending install.
        mHandler.sendEmptyMessage(MCS_BOUND);
    }

    break;
}

3.10 PackageMS::PackageHandler::doHandleMessage

case MCS_GIVE_UP: {
    if (DEBUG_INSTALL) Slog.i(TAG, "mcs_giveup too many retries");
    HandlerParams params = mPendingInstalls.remove(0);
    Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "queueInstall",
            System.identityHashCode(params));
    break;
}

3.11 PackageMS::InstallParams::handleStartCopy

public void handleStartCopy() throws RemoteException {
    int ret = PackageManager.INSTALL_SUCCEEDED;
    ...
    /*
     * If we have too little free space, try to free cache
     * before giving up.
     */
    mInstaller.freeCache(null, sizeBytes + lowThreshold);
    pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath,
            installFlags, packageAbiOverride);
    ...

    if (ret == PackageManager.INSTALL_SUCCEEDED) {
        ...
        if (selectInsLoc && volumeUuid == null) {
            String volumeuuid = null;
            final long sizeBytes = mContainerService.calculateInstalledSize(
                       origin.resolvedPath, isForwardLocked(), packageAbiOverride); // 安装大小
            volumeuuid = PackageHelper.resolveInstallVolume(mContext,
                       installerPackageName, pkgLite.installLocation, sizeBytes);
            ...
        }
    }
    ...
    final InstallArgs args = createInstallArgs(this); // 1. 根据安装位置创建InstallArgs 2. 根据安装参数修改apk安装位置
    mArgs = args;
    ...
    /*
     * No package verification is enabled, so immediately start
     * the remote call to initiate copy using temporary file.
     */
    ret = args.copyApk(mContainerService, true); // copy操作由MediaContainerService服务委托给DefaultContainerService::copy完成
    // app: data/local/tmp --> data/app/vmdl<回话ID>.tmp ,设置权限644
    // app so :/data/app/vmdl<回话ID>.tmp/lib/arm/
    mRet = ret;
}

3.11.2 handleStartCopy逻辑

handleStartCopy逻辑

3.11.3 InstallArgs相关结构

InstallArgs相关结构

3.11.3 HandlerParams相关结构

HandlerParams相关结构

3.12 PackageMS::InstallParams::handleReturnCode

void handleReturnCode() {
    // If mArgs is null, then MCS couldn't be reached. When it
    // reconnects, it will try again to install. At that point, this
    // will succeed.
    if (mArgs != null) {
        processPendingInstall(mArgs, mRet);
    }
}

3.13 PackageMS::processPendingInstall

private void processPendingInstall(final InstallArgs args, final int currentStatus) {
    // Queue up an async operation since the package installation may take a little while.
    mHandler.post(new Runnable() {
        public void run() {
            ...
            if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
                args.doPreInstall(res.returnCode); // 安装前处理 1. status != PackageManager.INSTALL_SUCCEEDED -> cleanUp
                synchronized (mInstallLock) {
                    installPackageTracedLI(args, res); // 3.14 最终 installPackageLI
                }
                args.doPostInstall(res.returnCode, res.uid); // 3.15
            }

            // A restore should be performed at this point if (a) the install
            // succeeded, (b) the operation is not an update, and (c) the new
            // package has not opted out of backup participation.
            final boolean update = res.removedInfo != null
                    && res.removedInfo.removedPackage != null;
            final int flags = (res.pkg == null) ? 0 : res.pkg.applicationInfo.flags;
            boolean doRestore = !update
                    && ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0);

            // Set up the post-install work request bookkeeping.  This will be used
            // and cleaned up by the post-install event handling regardless of whether
            // there's a restore pass performed.  Token values are >= 1.
            ...

            PostInstallData data = new PostInstallData(args, res);
            mRunningInstalls.put(token, data);

            if (res.returnCode == PackageManager.INSTALL_SUCCEEDED && doRestore) {
                // Pass responsibility to the Backup Manager.  It will perform a
                // restore if appropriate, then pass responsibility back to the
                // Package Manager to run the post-install observer callbacks
                // and broadcasts.
                ...
            }

            if (!doRestore) {
                ...
                Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0); // 3.16
                mHandler.sendMessage(msg);
            }
        }
    });
}

3.14 PackageMS::installPackageLI

PackageMS-installPackageLI

3.15 PackageMS::doPostInstall

if status != PackageManager.INSTALL_SUCCEEDED -> cleanUp    

3.16 PackageMS::PackageHandler::doHandleMessage

case POST_INSTALL: {
    ...
    if (data != null) {
        InstallArgs args = data.args;
        PackageInstalledInfo parentRes = data.res;

        final boolean grantPermissions = (args.installFlags
                & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0;
        final boolean killApp = (args.installFlags
                & PackageManager.INSTALL_DONT_KILL_APP) == 0;
        final String[] grantedPermissions = args.installGrantPermissions;

        // Handle the parent package
        handlePackagePostInstall(parentRes, grantPermissions, killApp,
                grantedPermissions, didRestore, args.installerPackageName,
                args.observer); // 3.17
        ...
} break;

3.17 PackageMS::handlePackagePostInstall

private void handlePackagePostInstall(PackageInstalledInfo res, boolean grantPermissions,
        boolean killApp, String[] grantedPermissions,
        boolean launchedForRestore, String installerPackage,
        IPackageInstallObserver2 installObserver) {
        ...
            // Send added for users that see the package for the first time
            sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
                    extras, 0 /*flags*/, null /*targetPackage*/,
                    null /*finishedReceiver*/, firstUsers);

            // Send added for users that don't see the package for the first time
            if (update) {
                extras.putBoolean(Intent.EXTRA_REPLACING, true);
            }
            sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName,
                    extras, 0 /*flags*/, null /*targetPackage*/,
                    null /*finishedReceiver*/, updateUsers);

            // Send replaced for users that don't see the package for the first time
            if (update) {
                sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
                        packageName, extras, 0 /*flags*/,
                        null /*targetPackage*/, null /*finishedReceiver*/,
                        updateUsers);
                sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED,
                        null /*package*/, null /*extras*/, 0 /*flags*/,
                        packageName /*targetPackage*/,
                        null /*finishedReceiver*/, updateUsers);
            }
            ...
}

通过广播通知系统,app安装成功


4.1 PackageMS::deletePackageAsUser

public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int userId,
        int flags) {
    deletePackage(packageName, new LegacyPackageDeleteObserver(observer).getBinder(), userId,
            flags);
}

4.2 PackageMS::deletePackage

public void deletePackage(final String packageName,
        final IPackageDeleteObserver2 observer, final int userId, final int deleteFlags) {
    // 权限检查
    mContext.enforceCallingOrSelfPermission(
            android.Manifest.permission.DELETE_PACKAGES, null);
    ...
    mHandler.post(new Runnable() {
        public void run() {
            mHandler.removeCallbacks(this);
            int returnCode;
            if (!deleteAllUsers) {
                returnCode = deletePackageX(packageName, userId, deleteFlags); // 4.3
            } else {
                    ...
                }
            }
            observer.onPackageDeleted(packageName, returnCode, null);
        } //end run
    });
}

4.3 PackageMS::deletePackageX

private int deletePackageX(String packageName, int userId, int deleteFlags) {
    ...
    /// M: Try to remove dex file.
    PackageSetting ps = mSettings.mPackages.get(packageName);
    if (ps != null) {
        if (isVendorApp(ps)) {
            ...
            mInstaller.rmdexcache(ps.pkg.baseCodePath, getPrimaryInstructionSet(ps.pkg.applicationInfo));
            ...
            }
        }
    }
    ...
    res = deletePackageLIF(packageName, UserHandle.of(removeUser), true, allUsers,
                        deleteFlags | REMOVE_CHATTY, info, true, null); // 4.6
    ...
    // Force a gc here.
    Runtime.getRuntime().gc();
    // Delete the resources here after sending the broadcast to let
    // other processes clean up before deleting resources.
    if (info.args != null) {
        synchronized (mInstallLock) {
            info.args.doPostDeleteLI(true); // 4.4
        }
    }
    return res ? PackageManager.DELETE_SUCCEEDED : PackageManager.DELETE_FAILED_INTERNAL_ERROR;
}

4.4 PackageMS::FileInstallArgs::doPostDeleteLI

boolean doPostDeleteLI(boolean delete) {
    // XXX err, shouldn't we respect the delete flag?
    cleanUpResourcesLI(); // 4.5
    return true;
}

4.5 PackageMS::FileInstallArgs::cleanUpResourcesLI

void cleanUpResourcesLI() {
    // Try enumerating all code paths before deleting
    List<String> allCodePaths = Collections.EMPTY_LIST;
    if (codeFile != null && codeFile.exists()) {
        try {
            final PackageLite pkg = PackageParser.parsePackageLite(codeFile, 0);
            allCodePaths = pkg.getAllCodePaths();
        } catch (PackageParserException e) {
            // Ignored; we tried our best
        }
    }

    cleanUp(); //removeCodePathLI
    removeDexFiles(allCodePaths, instructionSets);
}

4.6 PackageMS::deletePackageLIF

/*
 * This method handles package deletion in general
 */
private boolean deletePackageLIF(String packageName, UserHandle user,
        boolean deleteCodeAndResources, int[] allUserHandles, int flags,
        PackageRemovedInfo outInfo, boolean writeSettings,
        PackageParser.Package replacingPackage) {
    ...
    ret = deleteInstalledPackageLIF(ps, deleteCodeAndResources, flags, allUserHandles,
            outInfo, writeSettings, replacingPackage);
    ...
    return ret;
}

4.7 PackageMS::deleteInstalledPackageLIF

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

推荐阅读更多精彩内容