Skip to content

安装原子性

解释多包 session 的统一校验、提交屏障、冻结器和失败清理边界。

基于android-17.0.0_r1
AndroidPackageManagerServicePackageInstaller原子安装源码阅读

安装原子性 ​

本文承接 Session 安装、系统应用更新 和 安装回调。这里的“原子”特指 PMS 对一组安装请求的包状态提交:多包 session 要么整体进入 mPackages/Settings,要么所有请求都进入失败清理。它不声称外部通知、dexopt 进程副作用或远端 DataLoader 是一个不可分割的数据库事务。本文从 Android 17 InstallPackageHelper.installPackagesTraced 追踪 Prepare、Scan、Reconcile、Dexopt、Commit 的边界,并解释失败时 App ID、code path、freeze 和回调如何收尾。

1. 原子单元 ​

1.1 多包 session ​

PackageInstallerSession 的 parent session 持有 child sessions;提交时 PMS 将它们转换为一组 InstallRequest。parent 本身没有要安装的 APK,但它决定整组是否成功。一个 child 的签名、split、shared library 或版本检查失败,整组不能进入 commit。

1.2 保证范围 ​

保证实现位置不保证
所有包一起通过 reconcilereconcileInstallPackages外部网络传输事务
只有统一 commit 才写运行状态commitInstallPackages应用已启动进程的外部副作用
失败清理每个 request 的 code path/App IDcompleteInstallProcess客户端一定收到结果
旧包由 freezer 保护到提交结束PackageFreezer第三方服务调用自动回滚

2. 四阶段主线 ​

2.1 顶层编排 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

java
void installPackagesTraced(List<InstallRequest> requests, MoveInfo moveInfo) {
    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages");
    boolean pendingForDexopt = false;
    boolean success = false;
    final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
    final Map<String, Settings.VersionInfo> versionInfos =
            new ArrayMap<>(requests.size());
    final long acquireTime = acquireWakeLock(requests.size());
    try {
        if (prepareInstallPackages(requests)
                && scanInstallPackages(requests, createdAppId, versionInfos)) {
            final List<ReconciledPackage> reconciledPackages =
                    reconcileInstallPackages(requests, versionInfos);
            if (reconciledPackages == null) {
                return;
            }
            if (renameAndUpdatePaths(requests)) {
                pendingForDexopt = true;
                final Runnable actionsAfterDexopt = () -> doPostDexopt(
                        reconciledPackages, requests, createdAppId,
                        moveInfo, acquireTime);
                prepPerformDexoptIfNeeded(reconciledPackages, actionsAfterDexopt);
            }
        }
    } finally {
        if (!pendingForDexopt) {
            completeInstallProcess(requests, createdAppId, success);
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
            doPostInstall(requests, moveInfo);
            releaseWakeLock(acquireTime, requests.size());
        }
    }
}

源码注释明确要求任一阶段失败都导致整组安装失败。pendingForDexopt 是异步屏障:路径改名后,清理和 commit 延迟到 dexopt 回调;若 prepare、scan、reconcile 直接失败,则 finally 立即进入统一收尾。

2.2 Commit 写入点 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

java
private boolean commitInstallPackages(
        List<ReconciledPackage> reconciledPackages) {
    try (PackageManagerTracedLock installLock =
            mPm.mInstallLock.acquireLock()) {
        for (ReconciledPackage reconciledPkg : reconciledPackages) {
            final InstallRequest request = reconciledPkg.mInstallRequest;
            final PackageFreezer freezer = freezePackageForInstall(
                    request.getParsedPackage().getPackageName(),
                    UserHandle.USER_ALL, request.getInstallFlags(),
                    "installPackageLI",
                    ApplicationExitInfo.REASON_PACKAGE_UPDATED, request);
            request.setFreezer(freezer);
        }
        synchronized (mPm.mLock) {
            commitPackagesLocked(reconciledPackages,
                    mPm.mUserManager.getUserIds());
        }
        executePostCommitStepsLIF(reconciledPackages);
    }
    return true;
}

先持有 mInstallLock,再在 mLock 下执行 commitPackagesLocked;所有 package freezer 都创建成功后才进入统一写入。ReconciledPackage 是跨包一致性结果,commitPackagesLocked 是把结果变成当前 Settings、Resolver 和权限状态的唯一阶段。若冻结或 commit 抛错,调用方不会把部分成功报告为整组成功。

3. 失败收尾 ​

3.1 App ID 清理 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

java
private void completeInstallProcess(List<InstallRequest> requests,
        Map<String, Boolean> createdAppId, boolean success) {
    synchronized (mInstallingPackages) {
        for (InstallRequest request : requests) {
            final ParsedPackage parsedPkg = request.getParsedPackage();
            if (parsedPkg != null) {
                mInstallingPackages.remove(parsedPkg.getPackageName());
            }
        }
    }
    if (!success) {
        for (InstallRequest request : requests) {
            if (request.getParsedPackage() != null
                    && createdAppId.getOrDefault(
                            request.getParsedPackage().getPackageName(), false)) {
                cleanUpAppIdCreations(request);
            }
        }
    }
}

扫描阶段可能为新包预先注册 App ID。失败时必须按包名清理这些创建记录,否则下次安装会看到“已占用”的 UID。这个动作与删除 code path 分离:App ID 是 Settings/AppIds 状态,code path 是文件系统资源,两者都要收尾。

3.2 code path 清理 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

java
private void doPostInstallCleanUp(InstallRequest request,
        MoveInfo moveInfo) {
    if (moveInfo != null) {
        if (request.getReturnCode() == PackageManager.INSTALL_SUCCEEDED) {
            mRemovePackageHelper.cleanUpForMoveInstall(
                    moveInfo.mFromUuid, moveInfo.mPackageName,
                    moveInfo.mFromCodePath);
        } else {
            mRemovePackageHelper.cleanUpForMoveInstall(
                    moveInfo.mToUuid, moveInfo.mPackageName,
                    moveInfo.mFromCodePath);
        }
    } else if (request.getReturnCode()
            != PackageManager.INSTALL_SUCCEEDED) {
        mRemovePackageHelper.removeCodePath(request.getCodeFile());
    }
}

普通失败删除每个 request 的 code path;move install 则按成功/失败选择清理源卷或目标卷。多包原子性不意味着一个公共临时目录可以被一次性删除,清理仍以 request 为粒度。

3.3 dexopt 失败 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java

java
void doPostDexopt(List<ReconciledPackage> reconciledPackages,
        List<InstallRequest> requests,
        Map<String, Boolean> createdAppId,
        MoveInfo moveInfo, long acquireTime) {
    boolean isDexoptCompleted = true;
    for (InstallRequest request : requests) {
        request.onWaitDexoptFinished();
        if (request.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) {
            isDexoptCompleted = false;
        }
    }
    boolean success = false;
    try {
        if (isDexoptCompleted
                && commitInstallPackages(reconciledPackages)) {
            success = true;
        }
    } finally {
        completeInstallProcess(requests, createdAppId, success);
        doPostInstall(requests, moveInfo);
        releaseWakeLock(acquireTime, requests.size());
    }
}

dexopt 是 commit 前的屏障:任一 request 的 dexopt 返回失败,commitInstallPackages 不会被调用。这样系统不会出现“部分包已经写入 Settings、另一个包 dexopt 失败”的中间提交状态。

4. PackageFreezer ​

4.1 冻结目的 ​

冻结器阻止更新期间目标包进程继续使用旧 code/data。它由 PMS 创建并附着在每个 InstallRequest 上;request 结束或 POST_INSTALL 时关闭。冻结失败发生在 commit 前,属于整组失败条件。

4.2 锁顺序 ​

commitInstallPackages 先获取 mInstallLock,再短暂获取 mPm.mLock 执行状态提交;耗时的 dexopt、native extraction 和清理在锁外或不同阶段完成。锁顺序和 freezer 生命周期共同避免并发查询看到一半更新状态,也避免在持有包锁时调用可能反向获取 PMS 锁的外部服务。

5. staged 原子性 ​

staged session 的“重启后生效”由 StagingManager、PackageInstallerSession.StagedSession 和 apexd 协作。PMS 的 APK commit 仍遵守同一组 prepare/scan/reconcile/commit 规则,但 session 的 ready/applied/failed 状态要持久化到重启后再恢复。APEX 激活不是普通 APK 文件 rename;因此本文只把它作为 session 聚合的一员,不把 apexd 的底层事务细节归入 PMS commit。

6. 验证方法 ​

6.1 多包失败 ​

创建包含两个 APK 的 parent session,让其中一个 APK 使用错误签名或不存在的 split,再 commit。关键断言是两个包都没有出现在最终 mPackages,失败 child 和未失败 child 都收到失败结果,staging code path 被清理。这个实验覆盖 reconcile 的整组失败,不证明外部下载是否原子。

6.2 dexopt 失败 ​

在测试构建中注入 dexopt 失败,观察 doPostDexopt 不进入 commitInstallPackages,并检查 App ID、code path 和 freezer 都完成清理。若 Settings 已出现新版本,说明失败发生在 commit 之后,应转查 post-commit 清理,而不是把它归因于原子提交前检查。

6.3 并发查询 ​

安装更新期间持续执行:

bash
adb shell dumpsys package <package.name>
adb shell pm path <package.name>

关注更新前后版本、code path 和 user 状态是否一次性切换。查询结果不能证明所有组件注册是同一 CPU 指令完成的,但可观察到 PMS 对外暴露的是 commit 前旧状态或 commit 后新状态,而非一个缺少 Settings 的中间版本。

7. 源码路线 ​

  1. PackageInstallerSession parent/child session:确定原子单元边界。
  2. InstallPackageHelper.installPackagesTraced:观察阶段短路和 finally 收尾。
  3. reconcileInstallPackages:理解跨包一致性为何在 commit 前完成。
  4. renameAndUpdatePaths:确认文件路径准备早于 dexopt。
  5. doPostDexopt / commitInstallPackages:确认 dexopt 是提交屏障。
  6. commitPackagesLocked:跟踪 Settings、Resolver、权限的统一写入。
  7. completeInstallProcess / doPostInstallCleanUp:跟踪 App ID、code path 和 freezer 的失败清理。

安装原子性最终由三个条件共同构成:所有请求先完成可预判校验,commit 只在整组结果可用时执行,失败时每个请求的资源和状态都被回收。它不是抽象的“全有或全无”口号,而是由 request 集合、锁、freezer、dexopt 屏障和逐请求清理拼出的具体控制流。