Skip to content

用户应用卸载

解释第三方 APK 的单用户卸载、全量删除、保留数据和多用户状态分支。

基于android-17.0.0_r1
AndroidPackageManagerService卸载用户应用源码阅读

用户应用卸载 ​

本文承接 卸载全流程 和 DeletePackageHelper 入口,只研究非系统包的真实删除路径。系统应用的恢复语义见 系统应用卸载。本文回答:单用户卸载为何可能只改状态,DELETE_KEEP_DATA 保留什么,以及最后一个用户卸载时谁删除包设置、权限和 code path。

1. 删除分叉 ​

第三方 APK 位于 /data/app。单用户请求先把该用户设为未安装;如果其他用户仍安装,或 PMS 要保留未安装包,则 APK 和全局设置继续存在。只有全用户删除或已无其他用户使用时,才进入完整删除。

2. 单用户状态 ​

2.1 执行分支 ​

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

java
final boolean systemApp = PackageManagerServiceUtils.isSystemApp(ps);
final int userId = user == null ? USER_ALL : user.getIdentifier();

if ((!systemApp || (flags & PackageManager.DELETE_SYSTEM_APP) != 0)
        && userId != USER_ALL) {
    synchronized (mPm.mLock) {
        markPackageUninstalledForUserLPw(ps, user, flags, callingUid);
        if (!systemApp) {
            clearPackageStateAndReturn =
                    ps.isInstalledOnAnyOtherUser(allUserHandles, userId)
                    || mPm.shouldKeepUninstalledPackageLPr(packageName);
        } else {
            clearPackageStateAndReturn = true;
        }
    }
    if (clearPackageStateAndReturn) {
        mRemovePackageHelper.clearPackageStateForUserLIF(
                ps, userId, flags);
        mPm.scheduleWritePackageRestrictions(user);
        return;
    }
}

PackageSetting 持有 per-user installed 状态。第三方包单用户卸载时,markPackageUninstalledForUserLPw 先写状态;只有确认没有其他用户需要该 APK 且不需要保留包设置,才继续全量删除。调用方看到成功,只能说明指定用户不再安装,不能推出 APK 已经消失。

2.2 保留组件状态 ​

源码文件: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());
} else {
    enabledComponents = null;
    disabledComponents = null;
}
ps.setUserState(nextUserId, 0, 0, false, false, false,
        false, 0, null, COMPONENT_ENABLED_STATE_DEFAULT,
        enabledComponents, disabledComponents,
        INSTALL_REASON_UNKNOWN, UNINSTALL_REASON_UNKNOWN,
        null, null, 0, USER_MIN_ASPECT_RATIO_UNSET,
        archiveState, false, USER_MIN_ASPECT_RATIO_UNSET,
        PERSONAL_CONTEXT_MODE_UNSET);

Android 17 在保留数据时还保留组件启用集合和 archive state;完整卸载则清除这些用户设置。源码签名包含更多 per-user 字段,阅读时应以当前分支为准,不要套用旧版本 setUserState 参数表。

3. 全量删除 ​

3.1 记录删除信息 ​

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

java
private void deleteInstalledPackageLIF(PackageSetting ps,
        int userId, boolean deleteCodeAndResources, int flags,
        int[] allUserHandles, PackageRemovedInfo outInfo,
        boolean writeSettings) {
    synchronized (mPm.mLock) {
        outInfo.mUid = ps.getAppId();
        outInfo.mBroadcastAllowList =
                mPm.mAppsFilter.getVisibilityAllowList(
                        mPm.snapshotComputer(), ps, allUserHandles,
                        mPm.mSettings.getPackagesLocked());
    }
    mRemovePackageHelper.removePackageDataLIF(
            ps, userId, allUserHandles, outInfo, flags, writeSettings);
    if (deleteCodeAndResources) {
        outInfo.mArgs = new CleanUpArgs(
                ps.getName(), ps.getPathString());
    }
}

PackageRemovedInfo 在包状态被改写前保存 appId、广播可见性和 code path。RemovePackageHelper 先清理内部状态,再由 CleanUpArgs 在合适时机删除 APK 和资源;状态删除与物理文件删除不是同一个调用。

3.2 内部结构移除 ​

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

java
final boolean shouldDeletePackageSetting =
        shouldDeletePackageSetting(deletedPs, targetUserId,
                allUserHandles, flags);
clearPackageStateForUserLIF(deletedPs,
        shouldDeletePackageSetting ? UserHandle.USER_ALL : targetUserId,
        flags);
removePackageLI(packageName, (flags & DELETE_CHATTY) != 0);

if (!deletedPs.isSystem()) {
    // 保留 PackageSetting 时也不能继续使用旧 AndroidPackage
    deletedPs.setPkg(null);
}
if (shouldDeletePackageSetting) {
    synchronized (mPm.mLock) {
        outInfo.mIsAppIdRemoved =
                mPm.mSettings.removePackageAndAppIdLPw(packageName);
        if (!mPm.mSettings.isDisabledSystemPackageLPr(packageName)) {
            mPermissionManager.onPackageUninstalled(
                    packageName, deletedPs.getAppId(), deletedPs,
                    deletedPkg, sharedUserPkgs, UserHandle.USER_ALL);
        }
        mPm.mSettings.removeRenamedPackageLPw(deletedPs.getRealName());
    }
}

DELETE_KEEP_DATA 通常使 shouldDeletePackageSetting 为 false:包设置保留,但非系统包的 pkg 指针被置空,防止查询继续把它当作运行包。完全删除时才移除 appId、权限卸载记录和 renamed package。

3.3 数据 flags ​

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

java
int appDataDeletionFlags = FLAG_STORAGE_DE | FLAG_STORAGE_CE
        | FLAG_STORAGE_EXTERNAL;
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;
}
mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId,
        appDataDeletionFlags);

没有 DELETE_KEEP_DATA 时销毁 CE、DE 和 external data;保留数据时不销毁个人数据,archive 模式只清理 cache 与 code cache。ART profile 默认另行清理,除非 flags 要求保留。

4. 特殊条件 ​

4.1 增量包 ​

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

java
if (ps.isIncremental()) {
    // 增量 APK 的 ART 产物可能不在 APK 目录内
    deleteArtDexoptArtifacts(packageName);
}
deleteInstalledPackageLIF(ps, userId, deleteCodeAndResources,
        flags, allUserHandles, outInfo, writeSettings);

增量包必须显式清理外部 dexopt 产物,否则删除 APK 目录并不足以回收所有编译结果。

4.2 Shared library ​

删除前会查询 static shared library 或 SDK library 的使用者。存在必需客户端时拒绝删除;SDK library independence 只在所有客户端声明 optional 时可能放行。这个判断发生在包状态被移除前。

4.3 Profile 级联 ​

单用户删除成功后,父用户的 child profile 只有在 deleteAppWithParent 为 true 且包已安装时才递归删除。child 删除失败会返回 DELETE_FAILED_FOR_CHILD_PROFILE,因此父用户成功不代表 profile 树全部完成。

5. 验证方法 ​

5.1 多用户 ​

用户 0 和用户 10 都安装同一第三方 APK,仅对用户 0 卸载。断言用户 0 的 installed/data 状态被清理,用户 10 仍能解析该包且 pm path 仍存在;随后全用户卸载,确认包设置、权限和 code path 消失。

5.2 保留数据 ​

执行 pm uninstall -k,再安装同包名 APK。检查保留的 version/split 元数据、组件状态和数据目录;archive 模式还应清理 cache/code-cache。该实验只证明 PMS 保留了数据,不证明旧代码一定兼容新数据。

5.3 依赖与增量 ​

卸载被其他包依赖的 shared library host,预期收到依赖冲突;卸载增量包,检查 ART dexopt 产物也被清理。两组实验分别覆盖前置拒绝和额外资源路径。

bash
adb shell pm path <package.name>
adb shell dumpsys package <package.name> | grep -E 'installed=|versionCode|codePath'

6. 源码路线 ​

  1. deletePackageVersionedInternal:入口用户范围和策略。
  2. deletePackageX:冻结、版本和删除结果。
  3. executeDeletePackageLIF:单用户与全量分叉。
  4. deleteInstalledPackageLIF:删除信息和 code path。
  5. RemovePackageHelper.removePackageDataLIF:设置、权限和包对象移除。
  6. clearPackageStateForUserLIF:数据、cache、ART profile 和 archive。
  7. 增量、shared library、profile 级联:覆盖非正常分支。

用户应用卸载不是一个“删目录”函数:PackageSetting 管理用户状态,RemovePackageHelper 管理运行时结构与数据,CleanUpArgs 延迟处理 code path,PermissionManager 和 BroadcastHelper 消费删除结果。把这些对象分开,才能准确判断一次卸载到底只影响某个用户,还是已经让应用从整个设备消失。