保留数据卸载
本文承接 用户应用卸载、数据清理流程 和 降级安装。普通卸载会让包、数据和 appId 最终消失;pm uninstall -k 则故意留下一个“没有可运行 APK、但仍拥有历史数据和身份”的包设置。本文沿 Android 17 源码解释 DELETE_KEEP_DATA 如何穿过 shell、用户状态、RemovePackageHelper 和重新安装校验,并区分它与 archive、应用更新时临时保留数据的不同语义。
1. 状态模型
1.1 卸载后剩什么
执行 -k 后,APK code path 会被删除,mPackages 中的运行对象被移除,PackageSetting.getPkg() 被置空;但应用数据目录、appId、签名、versionCode、split revision 和部分 per-user 状态仍然保留。PMS 将这份 PackageSetting 当作旧数据的 owner,防止不同签名或未经允许的低版本 APK 接管数据。
1.2 与普通卸载对比
| 状态 | 普通卸载 | DELETE_KEEP_DATA |
|---|---|---|
| APK/code path | 删除 | 删除 |
| CE/DE/external data | 删除 | 保留 |
| ART profile | 默认删除 | 默认仍删除,除非另有 profile flag |
mPackages | 移除 | 移除 |
PackageSetting | 全量卸载时删除 | 保留 |
| appId | 释放 | 保留 |
PackageSetting.pkg | 对象随 setting 消失 | 显式置 null |
| 重装校验 | 按新安装处理 | 仍检查旧签名和版本 |
2. Shell 入口
2.1 -k 参数
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
int flags = 0;
int userId = UserHandle.USER_ALL;
long versionCode = PackageManager.VERSION_CODE_HIGHEST;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
case "-k":
flags |= PackageManager.DELETE_KEEP_DATA;
break;
case "--user":
userId = UserHandle.parseUserArg(getNextArgRequired());
break;
case "--versionCode":
versionCode = Long.parseLong(getNextArgRequired());
break;
default:
return 1;
}
}shell 只把 -k 转换成 delete flag,不执行数据操作。默认用户范围是 USER_ALL;指定 --user 时只影响目标用户,但如果其他用户仍安装该包,APK 本来也不会被删除。是否保留 PackageSetting 由 RemovePackageHelper 再次根据 flags 和用户数据判断。
3. 用户状态
3.1 标记未安装
源码文件:frameworks/base/services/core/java/com/android/server/pm/DeletePackageHelper.java
if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
enabledComponents = new ArraySet<>(
ps.readUserState(nextUserId).getEnabledComponents());
disabledComponents = new ArraySet<>(
ps.readUserState(nextUserId).getDisabledComponents());
}
final ArchiveState archiveState =
(flags & DELETE_KEEP_DATA) == 0
? null
: ps.getUserStateOrDefault(nextUserId).getArchiveState();
final long firstInstallTime =
(flags & DELETE_KEEP_DATA) == 0
? 0
: ps.getUserStateOrDefault(nextUserId)
.getFirstInstallTimeMillis();
ps.setUserState(nextUserId,
ps.getCeDataInode(nextUserId),
ps.getDeDataInode(nextUserId),
ps.getPccCeDataInode(nextUserId),
ps.getPccDeDataInode(nextUserId),
COMPONENT_ENABLED_STATE_DEFAULT,
false /* installed */,
true /* stopped */,
true /* notLaunched */,
false /* hidden */,
0 /* distractionFlags */,
null /* suspendParams */,
false /* instantApp */,
false /* virtualPreload */,
null /* lastDisableAppCaller */,
enabledComponents, disabledComponents,
PackageManager.INSTALL_REASON_UNKNOWN,
PackageManager.UNINSTALL_REASON_UNKNOWN,
null, null, firstInstallTime,
PackageManager.USER_MIN_ASPECT_RATIO_UNSET,
archiveState, appLockEnablementState,
PackageManager.VIRTUAL_GAMEPAD_USER_OPTION_UNSET,
PackageManager.PERSONAL_CONTEXT_MODE_UNSET);核心状态变为 installed=false、stopped=true、notLaunched=true,表示该用户不能启动包。CE/DE/PCC inode 沿用旧值,因为目录仍存在;组件启用集合、首次安装时间、archive state 和可选 App Lock 状态被保留。其他临时用户状态会重置。
3.2 Kernel mapping
mPm.mSettings.writeKernelMappingLPr(ps);修改用户状态后立即更新 kernel mapping,使 UID/包状态消费者不再把该用户视为已安装。保留 appId 不代表该用户还能启动应用;appId 是未来重新接管数据的身份锚点。
4. 数据清理分支
4.1 提前返回
源码文件:frameworks/base/services/core/java/com/android/server/pm/RemovePackageHelper.java
if ((flags & Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES) == 0) {
mAppDataHelper.destroyAppProfilesLIF(packageName);
}
int appDataDeletionFlags = FLAG_STORAGE_DE | FLAG_STORAGE_CE
| FLAG_STORAGE_EXTERNAL
| (flags & Installer.FLAG_CLEAR_APP_DATA_KEEP_ART_PROFILES);
if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
if ((flags & PackageManager.DELETE_ARCHIVE) != 0) {
mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
appDataDeletionFlags | Installer.FLAG_CLEAR_CACHE_ONLY);
mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
appDataDeletionFlags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
}
return;
}DELETE_KEEP_DATA 在数据目录销毁前返回,也跳过随后对 domain verification、preferred activity、PermissionManager 和 keystore 的普通卸载清理。这是因为设置和数据仍要为重新安装保留。ART profile 在 return 前处理,所以 -k 并不自动保留 profile;只有内部 KEEP_ART_PROFILES flag 才能改变它。
4.2 Archive 差异
archive 同时携带 DELETE_KEEP_DATA 与 DELETE_ARCHIVE,会额外清 cache 和 code cache,并保存 ArchiveState 供图标和恢复流程使用。普通 pm uninstall -k 不会创建 archive 体验,也不保证保留图标或一键恢复元数据。
5. 设置保留
5.1 删除判断
源码文件:frameworks/base/services/core/java/com/android/server/pm/RemovePackageHelper.java
private static boolean shouldDeletePackageSetting(
PackageSetting deletedPs, int userId,
int[] allUserHandles, int flags) {
if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
return false;
}
if (userId == UserHandle.USER_ALL) {
return true;
}
if (deletedPs.hasDataOnAnyOtherUser(allUserHandles, userId)) {
return false;
}
return true;
}DELETE_KEEP_DATA 是最高优先级条件,无论目标是单用户还是 USER_ALL,都不删除 PackageSetting。即使未设置 -k,其他用户仍有数据时也会保留 setting;这避免最后一个“已安装用户”卸载时误删另一个用户的孤立数据。
5.2 移除运行对象
clearPackageStateForUserLIF(deletedPs,
shouldDeletePackageSetting
? UserHandle.USER_ALL : targetUserId,
flags);
removePackageLI(packageName,
(flags & PackageManager.DELETE_CHATTY) != 0);
if (!deletedPs.isSystem()) {
deletedPs.setPkg(null);
}removePackageLI 从 mPackages、Resolver 等运行时结构移除包。由于 setting 仍然存在,源码显式把 pkg 置空,防止消费者继续使用已删除 APK 对应的 AndroidPackage。此时系统处于“有历史 setting,无 live package”的合法状态。
5.3 保留版本与 split
if (!deletedPs.isSystem() && !outInfo.mIsUpdate
&& outInfo.mRemovedUsers != null
&& !deletedPs.isExternalStorage()) {
synchronized (mPm.mLock) {
for (int userId : outInfo.mRemovedUsers) {
deletedPs.setInstalled(false, userId);
}
if (deletedPkg != null && deletedPkg.getSplitNames() != null) {
deletedPs.setSplitNames(deletedPkg.getSplitNames());
deletedPs.setSplitRevisionCodes(
deletedPkg.getSplitRevisionCodes());
}
}
}split 名称和 revision code 专门保存给后续降级检查使用。外部存储包不走这段状态保存,因为卷不可用与真正卸载的生命周期不同;应用更新替换旧 APK 时 outInfo.mIsUpdate=true,也不应把包标记为未安装。
6. 重装校验
6.1 数据 owner
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
final PackageSetting dataOwnerPs =
mPm.mSettings.getPackageLPr(packageName);
final AndroidPackage dataOwnerPkg = dataOwnerPs.getPkg();
if (dataOwnerPkg == null) {
// DELETE_KEEP_DATA 或 archived app:没有 live APK,但数据仍存在
if (!PackageManagerServiceUtils.isDowngradePermitted(
installFlags, dataOwnerPs.isDebuggable())) {
try {
PackageManagerServiceUtils.checkDowngrade(
dataOwnerPs, pkgLite);
} catch (PackageManagerException e) {
return Pair.create(
PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE,
"Downgrade detected on app uninstalled with"
+ " DELETE_KEEP_DATA: " + e.getMessage());
}
}
}pkg == null 不会让 PMS 把重安装作全新包处理。保留 setting 中的 version、base revision 和 split revision 仍参与降级判断,防止旧代码读取更高版本留下的数据。
6.2 签名与 appId
prepare/reconcile 还会用 dataOwnerPs.getSigningDetails() 检查新 APK 是否拥有 INSTALLED_DATA capability。签名通过后,新 AndroidPackage 复用保留的 appId 和数据目录;签名不匹配则返回 INSTALL_FAILED_UPDATE_INCOMPATIBLE,原数据不会被交给攻击者控制的同包名 APK。
6.3 required version
若调用方设置 requiredInstalledVersionCode,但 dataOwnerPkg==null,源码返回 INSTALL_FAILED_WRONG_INSTALLED_VERSION,因为包当前并未安装。保留 setting 的历史版本可以用于降级保护,却不能满足“某版本当前已安装”的并发前置条件。
7. 通知语义
7.1 数据未删除
outInfo.mDataRemoved =
(flags & PackageManager.DELETE_KEEP_DATA) == 0;卸载广播会携带 data removed 相关语义。-k 下包对用户不可用,但数据仍在,因此消费者不能收到“数据已经完全移除”的等价信号,appId 也通常不会作为彻底释放处理。
7.2 代码仍删除
deleteInstalledPackageLIF 仍为 CleanUpArgs 保存旧 code path,广播后由 cleanUpResources 删除 APK、native 和相关 code 资源。保留的是应用数据与身份设置,不是 APK 本身。
8. 失败与清理
8.1 重装失败
签名或版本校验失败时,新的 staging/code path 被清理,保留 setting 和旧数据继续存在。失败不会自动转为完整卸载,也不会重置历史版本;调用方需要显式执行不带 -k 的删除才能彻底清理。
8.2 孤立数据
如果 APK 永远不重装,保留 setting、appId 和数据会持续占用空间。系统的存储管理、归档或后续完整卸载可以清理它们;仅删除 /data/app 目录不会同步移除这些 PMS 状态。
9. 验证方法
9.1 状态与目录
安装应用并写入数据库,执行:
adb shell pm uninstall -k <package.name>
adb shell dumpsys package <package.name>
adb shell pm path <package.name>关键断言是 pm path 找不到 live APK,dumpsys 仍可能保留 setting/version/appId,数据目录仍存在且用户状态为未安装。该实验不能证明 ART profile 被保留,因为默认会清理 profile。
9.2 签名保护
用不同签名的同包名 APK 重装,预期 INSTALL_FAILED_UPDATE_INCOMPATIBLE;再用原签名重装,数据可重新访问且 appId 保持一致。这验证了保留 setting 是数据访问控制的一部分。
9.3 降级保护
先安装 version 20,-k 卸载,再安装同签名 version 10。未请求/允许降级时应返回 INSTALL_FAILED_VERSION_DOWNGRADE;版本 21 应通过。若只降低同名 split revision,也应由保存的 split 信息拒绝。
9.4 Archive 对照
比较普通 -k 和 archive:两者都保留数据,但 archive 会保留 ArchiveState 并清 cache/code-cache。不要用是否存在用户数据作为区分二者的唯一依据。
10. 源码路线
PackageManagerShellCommand.runUninstall:确认-k到 delete flag。DeletePackageHelper.markPackageUninstalledForUserLPw:跟踪 per-user 保留字段。RemovePackageHelper.clearPackageStateForUserLIF:观察 profile、archive 与提前 return。shouldDeletePackageSetting:确认 setting/appId 为什么保留。removePackageDataLIF:跟踪mPackages移除、pkg=null和 split 元数据。InstallPackageHelper.verifyReplacingVersionCode:理解无 live package 时的降级检查。PackageManagerServiceUtils.verifySignatures:理解重装接管旧数据的签名能力。
保留数据卸载创建的是一种明确的中间状态:代码已经移除,用户不能运行包,但 Settings 仍保存数据身份和历史版本。正是这份状态让重装可以复用 appId 与目录,同时继续拒绝错误签名和危险降级。把 PackageSetting 当作 data owner,而不是把 /data/user 目录当作全部事实,才能真正理解 pm uninstall -k。
