多用户卸载
本文承接 用户应用卸载、保留数据卸载 和 卸载回调。Android 的 APK code 通常由所有用户共享,但 installed、数据目录、权限和组件状态按用户保存。因此“为用户 10 卸载”与“从设备删除包”不是同一件事。本文沿 Android 17 源码追踪目标用户集合如何产生、单用户删除何时升级为全量清理、profile 如何级联、block uninstall 为何可能造成部分成功,以及广播如何按普通用户与 instant user 分组。
1. 多用户模型
1.1 全局与用户状态
| 对象 | 作用域 | 典型 owner |
|---|---|---|
| APK/code path | 设备全局 | PackageSetting、文件系统 |
| appId | 设备全局 | Settings AppIds |
AndroidPackage | 设备全局运行对象 | mPackages |
| installed/stopped/enabled | 每用户 | PackageUserState |
| CE/DE data | 每用户 | AppData/installd |
| runtime permission | 每用户 | PermissionManager |
单用户卸载首先改变 per-user 状态。只有没有其他用户需要 code、且没有必须保留的数据或设置时,PMS 才删除全局对象。
2. 目标用户
2.1 用户数组
源码文件:frameworks/base/services/core/java/com/android/server/pm/DeletePackageHelper.java
final boolean deleteAllUsers =
(deleteFlags & PackageManager.DELETE_ALL_USERS) != 0;
final int[] users = deleteAllUsers
? mUserManagerInternal.getUserIds()
: new int[] { userId };
if (UserHandle.getUserId(callingUid) != userId
|| (deleteAllUsers && users.length > 1)) {
mPm.mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
"deletePackage for user " + userId);
}users 是策略检查的目标集合。跨用户删除需要 INTERACT_ACROSS_USERS_FULL;DELETE_ALL_USERS 并不是简单把 userId 改成 USER_ALL 后跳过权限检查。后续 device admin、protected data 和 block uninstall 都基于这个集合评估。
2.2 设备策略遍历
for (int user : users) {
if (mPm.isPackageDeviceAdmin(packageName, user)) {
observer.onPackageDeleted(packageName,
PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER,
null);
return;
}
if (mPm.mProtectedPackages.isPackageDataProtected(
user, packageName)) {
observer.onPackageDeleted(packageName,
PackageManager.DELETE_FAILED_INTERNAL_ERROR,
null);
return;
}
}active device admin 或 protected package data 对目标集合中的任一用户成立时,入口直接拒绝整次请求。此时不会像 block uninstall 分支那样删除部分用户;两类策略的原子性不同。
3. 单用户卸载
3.1 状态先变更
源码文件:frameworks/base/services/core/java/com/android/server/pm/DeletePackageHelper.java
if (userId != USER_ALL) {
synchronized (mPm.mLock) {
markPackageUninstalledForUserLPw(
ps, user, flags, callingUid);
if (!systemApp) {
clearPackageStateAndReturn =
ps.isInstalledOnAnyOtherUser(
mUserManagerInternal.getUserIds(), userId)
|| mPm.shouldKeepUninstalledPackageLPr(packageName);
} else {
clearPackageStateAndReturn = true;
}
}
if (clearPackageStateAndReturn) {
mRemovePackageHelper.clearPackageStateForUserLIF(
ps, userId, flags);
mPm.scheduleWritePackageRestrictions(user);
return;
}
}目标用户先被标记为未安装。其他用户仍 installed 时,仅清理目标用户的数据、权限、域名和 preferred activities,保留共享 APK、appId 和全局包对象。若这是最后一个安装用户,流程继续检查是否还有其他用户数据,再决定是否删除 PackageSetting。
3.2 遗留数据
源码文件: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;
}最后一个 installed 用户被删除后,其他用户仍可能留有数据,例如先前使用 -k 卸载。只要 hasDataOnAnyOtherUser 为 true,PMS 就保留 PackageSetting,避免释放 appId 后留下无法安全归属的数据。
4. Profile 级联
4.1 父用户成功后触发
源码文件:frameworks/base/services/core/java/com/android/server/pm/DeletePackageHelper.java
returnCode = deletePackageX(internalPackageName, versionCode,
userId, deleteFlags, false, callingUid);
if (returnCode == DELETE_SUCCEEDED && packageState != null) {
final int[] childUserIds =
mUserManagerInternal.getProfileIds(userId, true);
for (int childId : childUserIds) {
if (childId == userId) {
continue;
}
if (mUserManagerInternal.getProfileParentId(childId) != userId) {
continue;
}
if (!packageState.getUserStateOrDefault(childId)
.isInstalled()) {
continue;
}
final UserProperties properties =
mUserManagerInternal.getUserProperties(childId);
if (properties != null
&& properties.getDeleteAppWithParent()) {
final int childResult = deletePackageX(
internalPackageName, versionCode, childId,
deleteFlags, false, callingUid);
if (childResult != DELETE_SUCCEEDED) {
returnCode =
PackageManager.DELETE_FAILED_FOR_CHILD_PROFILE;
}
}
}
}级联的前置条件有四个:父用户删除成功、确实是直接 child profile、child 中包已安装、deleteAppWithParent=true。源码使用删除前取得的 packageState 判断 child 是否安装;child 删除仍走完整的 deletePackageX 策略与数据清理。
4.2 失败语义
父用户已经成功后,某个 child 删除失败不会恢复父用户;最终 observer 返回 DELETE_FAILED_FOR_CHILD_PROFILE。这是部分完成结果,调用方必须逐用户查询 installed 状态,而不能只把非成功码解释为“所有用户均未改变”。
5. 全用户卸载
5.1 无阻止用户
final int[] blocked = getBlockUninstallForUsers(
innerSnapshot, internalPackageName, users);
if (ArrayUtils.isEmpty(blocked)) {
returnCode = deletePackageX(internalPackageName,
versionCode, userId, deleteFlags,
false, callingUid);
}无人阻止时,保留 DELETE_ALL_USERS 调用一次 deletePackageX。内部 removeUser=USER_ALL,PackageRemovedInfo.mRemovedUsers 会收集所有 installed 或仍有数据的用户,数据和包设置按全局范围清理。
5.2 部分阻止
} else {
final int userFlags =
deleteFlags & ~PackageManager.DELETE_ALL_USERS;
for (int targetUserId : users) {
if (!ArrayUtils.contains(blocked, targetUserId)) {
final int result = deletePackageX(
internalPackageName, versionCode,
targetUserId, userFlags,
false, callingUid);
if (result != DELETE_SUCCEEDED) {
Slog.w(TAG, "Package delete failed for user "
+ targetUserId + ", returnCode " + result);
}
}
}
returnCode = PackageManager.DELETE_FAILED_OWNER_BLOCKED;
}存在 block user 时,源码清除 DELETE_ALL_USERS,对其他用户逐个执行单用户删除,最后固定返回 owner blocked。共享 APK 是否仍存在取决于被阻止用户是否继续安装或拥有数据;这条路径刻意不是全有或全无。
5.3 block 集合
private int[] getBlockUninstallForUsers(
Computer snapshot, String packageName, int[] userIds) {
int[] result = EMPTY_INT_ARRAY;
for (int userId : userIds) {
if (snapshot.getBlockUninstallForUser(
packageName, userId)) {
result = ArrayUtils.appendInt(result, userId);
}
}
return result;
}block uninstall 是 per-user 状态,由 Computer snapshot 读取。全用户请求必须先形成完整 blocked 集合,才能决定一次全量调用还是多个单用户调用。
6. 广播用户
6.1 删除前采集
源码文件:frameworks/base/services/core/java/com/android/server/pm/DeletePackageHelper.java
outInfo.mRemovedUsers = userId == USER_ALL
? ps.queryUsersInstalledOrHasData(allUserHandles)
: new int[] { userId };
outInfo.populateBroadcastUsers(ps);mRemovedUsers 在状态修改前确定。全用户删除不仅包含 installed 用户,还包含仍有数据的用户;单用户删除只包含目标用户。该数组供广播、metrics 和 sequence number 更新共同消费。
6.2 广播分组
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageRemovedInfo.java
public void populateBroadcastUsers(
PackageSetting deletedPackageSetting) {
if (mRemovedUsers == null) {
mBroadcastUsers = null;
return;
}
mBroadcastUsers = EMPTY_INT_ARRAY;
mInstantUserIds = EMPTY_INT_ARRAY;
for (int i = mRemovedUsers.length - 1; i >= 0; --i) {
final int userId = mRemovedUsers[i];
if (deletedPackageSetting.getInstantApp(userId)) {
mInstantUserIds = ArrayUtils.appendInt(
mInstantUserIds, userId);
} else {
mBroadcastUsers = ArrayUtils.appendInt(
mBroadcastUsers, userId);
}
}
}广播按用户原来的 instant-app 状态拆分为两组。这个分类必须在 markPackageUninstalledForUserLPw 清除 instant 状态前完成,否则广播消费者会丢失原始类型。
7. 时序总图
8. 验证方法
8.1 其他用户仍安装
在用户 0、10 安装同一第三方包,只删除用户 0。断言用户 0 的 installed=false、数据与权限清理,用户 10 仍可启动,APK code path 和 appId 保留。这验证“code 全局、安装状态 per-user”。
8.2 最后用户
先让用户 10 使用 -k 卸载留下数据,再删除用户 0 的最后一个 installed 状态。断言 hasDataOnAnyOtherUser 阻止删除 PackageSetting/appId;随后完整清理用户 10 数据,才允许全局设置消失。
8.3 Profile 级联
建立 parent 与两个 child profile,一个设置 deleteAppWithParent=true,另一个为 false。删除 parent 后,断言只有前者级联;注入 child 删除失败时,observer 返回 DELETE_FAILED_FOR_CHILD_PROFILE,parent 仍保持已删除。
8.4 部分阻止
为一个用户设置 block uninstall,再发起 DELETE_ALL_USERS。断言未阻止用户被逐个卸载,阻止用户保留,最终结果为 DELETE_FAILED_OWNER_BLOCKED。这是本文最重要的非原子分支。
adb shell pm list users
adb shell pm list packages --user <userId> | grep <package.name>
adb shell dumpsys package <package.name> | grep -E 'User|installed=|dataDir'9. 源码路线
deletePackageVersionedInternal:目标 users、跨用户权限和策略遍历。deletePackageX:removeUser、用户状态快照和 freezer 范围。executeDeletePackageLIF:单用户 installed 状态与全量删除分叉。shouldDeletePackageSetting:理解其他用户数据为何保留 appId。- parent/child profile 循环:跟踪
deleteAppWithParent与部分失败。 getBlockUninstallForUsers:跟踪全用户请求的部分执行。PackageRemovedInfo.populateBroadcastUsers:理解广播用户集合。
多用户卸载的关键不是 USER_ALL 常量,而是区分三层状态:共享的 APK/appId、每用户 installed/data、profile 和设备策略关系。DeletePackageHelper 先形成目标用户集合,再按策略决定一次全量删除或多次单用户删除;RemovePackageHelper 最后依据其他用户的数据决定是否真正移除 PackageSetting。沿着这三层阅读,才能正确解释最后一个用户、孤立数据、profile 级联和部分阻止场景。
