Skip to content

保留数据卸载

解释 pm uninstall -k 如何保留数据和 PackageSetting,以及重装时如何重新接管旧状态。

基于android-17.0.0_r1
AndroidPackageManagerServiceDELETE_KEEP_DATA卸载源码阅读

保留数据卸载 ​

本文承接 用户应用卸载、数据清理流程 和 降级安装。普通卸载会让包、数据和 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

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

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 ​

java
mPm.mSettings.writeKernelMappingLPr(ps);

修改用户状态后立即更新 kernel mapping,使 UID/包状态消费者不再把该用户视为已安装。保留 appId 不代表该用户还能启动应用;appId 是未来重新接管数据的身份锚点。

4. 数据清理分支 ​

4.1 提前返回 ​

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

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

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 移除运行对象 ​

java
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 ​

java
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

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 数据未删除 ​

java
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 状态与目录 ​

安装应用并写入数据库,执行:

bash
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. 源码路线 ​

  1. PackageManagerShellCommand.runUninstall:确认 -k 到 delete flag。
  2. DeletePackageHelper.markPackageUninstalledForUserLPw:跟踪 per-user 保留字段。
  3. RemovePackageHelper.clearPackageStateForUserLIF:观察 profile、archive 与提前 return。
  4. shouldDeletePackageSetting:确认 setting/appId 为什么保留。
  5. removePackageDataLIF:跟踪 mPackages 移除、pkg=null 和 split 元数据。
  6. InstallPackageHelper.verifyReplacingVersionCode:理解无 live package 时的降级检查。
  7. PackageManagerServiceUtils.verifySignatures:理解重装接管旧数据的签名能力。

保留数据卸载创建的是一种明确的中间状态:代码已经移除,用户不能运行包,但 Settings 仍保存数据身份和历史版本。正是这份状态让重装可以复用 appId 与目录,同时继续拒绝错误签名和危险降级。把 PackageSetting 当作 data owner,而不是把 /data/user 目录当作全部事实,才能真正理解 pm uninstall -k。