安装原子性
本文承接 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 保证范围
| 保证 | 实现位置 | 不保证 |
|---|---|---|
| 所有包一起通过 reconcile | reconcileInstallPackages | 外部网络传输事务 |
| 只有统一 commit 才写运行状态 | commitInstallPackages | 应用已启动进程的外部副作用 |
| 失败清理每个 request 的 code path/App ID | completeInstallProcess | 客户端一定收到结果 |
| 旧包由 freezer 保护到提交结束 | PackageFreezer | 第三方服务调用自动回滚 |
2. 四阶段主线
2.1 顶层编排
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.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
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
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
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
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 并发查询
安装更新期间持续执行:
adb shell dumpsys package <package.name>
adb shell pm path <package.name>关注更新前后版本、code path 和 user 状态是否一次性切换。查询结果不能证明所有组件注册是同一 CPU 指令完成的,但可观察到 PMS 对外暴露的是 commit 前旧状态或 commit 后新状态,而非一个缺少 Settings 的中间版本。
7. 源码路线
PackageInstallerSessionparent/child session:确定原子单元边界。InstallPackageHelper.installPackagesTraced:观察阶段短路和 finally 收尾。reconcileInstallPackages:理解跨包一致性为何在 commit 前完成。renameAndUpdatePaths:确认文件路径准备早于 dexopt。doPostDexopt/commitInstallPackages:确认 dexopt 是提交屏障。commitPackagesLocked:跟踪 Settings、Resolver、权限的统一写入。completeInstallProcess/doPostInstallCleanUp:跟踪 App ID、code path 和 freezer 的失败清理。
安装原子性最终由三个条件共同构成:所有请求先完成可预判校验,commit 只在整组结果可用时执行,失败时每个请求的资源和状态都被回收。它不是抽象的“全有或全无”口号,而是由 request 集合、锁、freezer、dexopt 屏障和逐请求清理拼出的具体控制流。
