安装失败清理
本文面向已经读过 安装全景、安装后注册 和 压缩系统 Stub 包 的读者。本文只讨论安装失败后的撤销与清理,不重复成功安装、卸载策略或 ART 编译流程。核心问题是:解析失败、扫描失败、移动失败或包移动失败时,PMS 怎样知道应该删哪条 code path,是否要删 app data,以及为什么一个失败安装可能仍保留 PackageSetting。
1. 清理责任
源码文件:frameworks/base/services/core/java/com/android/server/pm/RemovePackageHelper.java、frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java。
InstallPackageHelper 决定何时失败;RemovePackageHelper 执行代码目录、app data、权限和内部 package object 的清理。失败安装通常只删除本次产生的临时 code path,不能套用完整卸载的“删除所有用户数据”。
2. 扫描失败
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java;符号:扫描结果处理分支。
当 data app 解析或扫描失败,PMS 会记录错误并调用 mRemovePackageHelper.removeCodePath(result.scanFile):
if ((scanParams.scanFlags & SCAN_AS_SYSTEM) == 0
&& errorCode != PackageManager.INSTALL_SUCCEEDED) {
logCriticalInfo(Log.WARN,
"Deleting invalid package at " + result.scanFile);
mRemovePackageHelper.removeCodePath(result.scanFile);
}system 分区包不走同一删除分支,因为 system code path 不能被当作 data 临时安装目录删除;APEX-in-APEX 失败还会通知 ApexManager.reportErrorWithApkInApex。因此“扫描失败后文件还在”要先确认路径属于 system、data 还是 APEX。
3. removeCodePath
源码文件:frameworks/base/services/core/java/com/android/server/pm/RemovePackageHelper.java。
removeCodePath 在 mInstallLock 下进入 removeCodePathLI。目录路径由 Installer 删除;incremental path 还要通过 IncrementalManager.rmPackageDir 删除对应 package directory。若目录父级是随机目录,父目录也会被删除并清理 PackageCacher 缓存。
if (isIncremental) {
mIncrementalManager.rmPackageDir(
needRemoveParent ? codePathParent : codePath);
}
mInstaller.rmPackageDir(packageName, codePath.getAbsolutePath());
if (needRemoveParent) {
mInstaller.rmPackageDir(packageName,
codePathParent.getAbsolutePath());
removeCachedResult(codePathParent);
}Installer 异常只记录 warning,不向上重新抛出。也就是说安装可以已经报告失败,但磁盘残留需要下一次 cleanup 或人工诊断;不能把 removeCodePath 返回无异常当成“目录一定删除”。
4. 安装后失败
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java;符号:doPostInstallCleanUp。
安装流程完成主事务后,doPostInstallCleanUp 按 request return code 决定清理:
if (moveInfo != null) {
if (request.getReturnCode() == INSTALL_SUCCEEDED) {
mRemovePackageHelper.cleanUpForMoveInstall(
moveInfo.mFromUuid, moveInfo.mPackageName,
moveInfo.mFromCodePath);
} else {
mRemovePackageHelper.cleanUpForMoveInstall(
moveInfo.mToUuid, moveInfo.mPackageName,
moveInfo.mFromCodePath);
}
} else if (request.getReturnCode() != INSTALL_SUCCEEDED) {
mRemovePackageHelper.removeCodePath(request.getCodeFile());
}move install 失败时清理目标卷;普通安装失败时清理 request 的 code file。app data cleanup 由更具体的失败阶段决定,不是这里无条件销毁。
5. 内存对象清理
成功扫描并注册过的包,失败回滚需要从 mPackages、ComponentResolver、PermissionManager、PackageProperty、Instrumentation 和 shared library 表中移除。removePackageLI 在 mInstallLock 下进入 mLock,调用 cleanPackageDataStructuresLILPw 执行这些消费者的反向更新。
synchronized (mPm.mLock) {
final AndroidPackage removedPackage =
mPm.mPackages.remove(packageName);
if (removedPackage != null) {
cleanPackageDataStructuresLILPw(
removedPackage,
AndroidPackageLegacyUtils.isSystem(removedPackage),
chatty);
}
}移除 mPackages 不等于移除 Settings。removePackageDataLIF 还要根据 DELETE_KEEP_DATA、其他 user 数据和 system disabled package 判断是否删除 PackageSetting;失败安装可能保留 setting 以支持后续恢复或清理。
6. 用户数据分支
clearPackageStateForUserLIF 首先清 ART profiles,再决定是否销毁 app data。DELETE_KEEP_DATA 会保留个人数据;archive 例外只清 cache/code-cache。没有 KEEP_DATA 时才调用 AppDataHelper.destroyAppDataLIF,并清除 CE/DE/PCC inode。
源码文件:frameworks/base/services/core/java/com/android/server/pm/RemovePackageHelper.java;符号:clearPackageStateForUserLIF。
if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
if ((flags & PackageManager.DELETE_ARCHIVE) != 0) {
mAppDataHelper.clearAppDataLIF(... FLAG_CLEAR_CACHE_ONLY);
mAppDataHelper.clearAppDataLIF(... FLAG_CLEAR_CODE_CACHE_ONLY);
}
return;
}这解释了 partial install cleanup 的要求:调用方必须传入正确 KEEP_DATA 语义,否则“清理失败安装”可能误删本应保留的数据,或留下应该删除的 app data。
7. shared UID 与库
清理内部 package object 时,system 包持有的 shared libraries、SDK library 和 static shared library 会从 SharedLibrariesImpl 移除。完全删除 PackageSetting 后,PermissionManager 还要处理 shared user;如果 shared UID 仍有其他成员,appId 和权限状态不能一起删除。
因此失败回滚不能只删除 APK 文件:下次安装同包名/同 shared UID 时,残留 library 或权限状态会改变 reconcile 结果。
8. Move install
cleanUpForMoveInstall 在 mInstallLock 下销毁目标卷上的 DE/CE 数据和 code path,但显式保留 ART profiles,因为 move 只改变存储位置,删除唯一 profile 副本会破坏后续优化。成功移动清理旧卷;失败移动清理目标卷。
final int flags = FLAG_STORAGE_DE | FLAG_STORAGE_CE
| Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES;
for (int userId : userIds) {
mPm.mInstaller.destroyAppData(volumeUuid,
packageName, userId, flags, 0, 0);
}
removeCodePathLI(codeFile);移动失败的 cleanup 目标不是“原包全部删除”,而是回收新卷上已经创建的半成品,同时保留原卷和 profiles 的可恢复性。
9. 锁与顺序
RemovePackageHelper 的代码删除和数据清理要求 mInstallLock;内部 mPackages、Settings、resolver 和权限表要求 mLock。Android 17 的典型顺序是先 install lock,再进入 PMS lock;反向获取可能造成安装/删除死锁。
失败清理是可重复调用的:路径不存在时 removeCodePathLI 直接返回;但内部状态和磁盘状态可能只完成一部分,后续 cleanup 必须分别验证。
10. 失败定位
| 现象 | 检查点 | 解释 |
|---|---|---|
| 失败 APK 仍在 data/app | removeCodePath 日志、Installer 异常 | 清理失败会记录 warning,不一定抛异常 |
| mPackages 无包但 Settings 有 setting | removePackageDataLIF / KEEP_DATA | 内存对象和 PackageSetting 生命周期不同 |
| shared UID 下次安装异常 | library/permission cleanup | 共享成员存在时不能删除整个 appId |
| move 失败后原包不可恢复 | cleanUpForMoveInstall 卷参数 | 失败时应清目标卷,不应删原卷 |
| stub 失败后 system 包不可启动 | stub restore/disable 分支 | stub 必须恢复但最终保持 disabled |
| 失败安装误删用户数据 | DELETE_KEEP_DATA/ARCHIVE flags | 数据清理范围由 flags 决定 |
11. 源码练习
- 从 data app 扫描失败分支追到
removeCodePath,说明为什么 system path、data path 和 APK-in-APEX 的处理不同。 - 构造 move install 失败,判断
cleanUpForMoveInstall的 volume、profile flag 和 code path 目标。 - 对比
removePackageLI、removePackageDataLIF和removeCodePathLI,列出各自删除的对象以及不会删除的对象。 - 给定失败安装后
mPackages已清空但 PackageSetting 仍存在,沿 KEEP_DATA、shared UID、其他 user data 三个条件判断是否符合 Android 17 的设计。
