15. 多用户支持:UserManager 与 PMS 的协作,每个用户独立的包管理状态
多用户支持,说白了就是一台 Android 设备能同时给好几个人用,而且每个人的数据、应用、设置都是隔离的。这个功能在平板、企业设备、学生机上特别常见。我最早接触这块是在做企业定制 ROM 的时候,客户要求一台平板给不同班的学生轮流用,每个学生登录后只能看到自己的作业和 App。嗯,那时候我才真正开始研究 UserManager 和 PMS 是怎么配合的。
15.1 多用户架构概览
Android 的多用户系统,核心思路是「一人一个用户 ID(UID),一人一套数据目录」。每个用户都有自己的 /data/user/<userId> 目录,里面装着该用户的 App 数据、SharedPreferences、数据库等。
PMS 在管理包信息时,会为每个用户维护一份独立的「已安装应用列表」和「权限授予状态」。你想想看,用户 A 安装了微信,用户 B 的桌面上就不会出现微信图标——除非 B 也主动安装一次(或者系统做了「应用克隆」)。
这里有个关键点:APK 文件是共享的,但每个用户的数据目录是独立的。APK 存在 /data/app/ 下,所有用户共用同一份二进制代码。但每个用户启动 App 时,PMS 会分配不同的 UID,并挂载不同的数据目录。
核心概念:多用户 ≠ 多进程。每个用户运行 App 时,进程的 UID 不同,但 APK 代码段在内存中只有一份(通过 Linux 的共享库机制)。
15.2 UserManager 的角色
UserManager 是上层 API 的入口。应用可以通过 Context.getSystemService(UserManager.class) 获取它。它的主要职责包括:
- 创建/删除用户(需要系统权限)
- 查询当前设备上的用户列表
- 判断当前用户是否为管理员(Primary User)
- 获取用户序列号、用户类型(普通用户/访客/受限用户)
- 监听用户切换事件
我个人习惯在调试多用户问题时,先用 adb 命令看看当前用户情况:
adb shell dumpsys user
# 输出示例:
# Users:
# UserInfo{0:Owner:13} serialNo=0
# Created: 2024-01-15
# Last logged in: 2024-06-20
# UserInfo{10:Guest:14} serialNo=1
# Created: 2024-03-10
# Last logged in: 2024-06-19
UserManager 底层通过 Binder 调用 IUserManager 服务,这个服务运行在 SystemServer 进程中。它维护着用户信息数据库(/data/system/users/ 目录下的 XML 文件)。
15.3 PMS 如何为每个用户管理包状态
PMS 内部维护了一个 mSettings 对象,里面包含一个 mPackages 的 Map。但你可能不知道,每个 Package 对象里还嵌套了一个 mUsers 的 SparseArray,专门记录每个用户对该包的状态。
举个例子,当用户 A 安装了一个 App,PMS 会做以下几件事:
- 解析 APK,生成 Package 对象(全局唯一)
- 在 Package 对象的
mUsers中,添加用户 A 的记录,标记为「已安装」 - 为用户 A 创建数据目录 /data/user/<userId>/<packageName>/
- 分配 UID(如果该 App 之前没被任何用户安装过,则分配新 UID;否则复用)
- 写入 packages.xml,持久化用户 A 的安装状态
当用户 B 也安装同一个 App 时,PMS 发现 Package 对象已经存在,于是只在 mUsers 中给用户 B 加一条记录,并创建 B 的数据目录。APK 文件不会重复拷贝。
避坑指南:我曾经遇到过一个问题:用户 A 卸载了某个 App,但用户 B 还在用。结果 PMS 把 APK 文件也删了,导致用户 B 的 App 崩溃。后来才发现,PMS 在卸载时只会检查当前用户是否还有引用,如果其他用户也在用,APK 文件会保留。但如果你用 pm uninstall --user all 强制卸载所有用户,那就真的全没了。
15.4 用户切换时的 PMS 行为
当系统切换用户时,PMS 会收到 UserManager 发出的 USER_SWITCHING 广播。PMS 的处理逻辑大致如下:
- 冻结旧用户的 App 进程(发送 SIGSTOP 信号)
- 解冻新用户的 App 进程(发送 SIGCONT 信号)
- 更新 Launcher 的 App 列表,只显示新用户已安装的应用
- 重新挂载存储空间(如果使用了文件级加密)
这里有个性能问题:如果旧用户开了很多 App,切换时全部冻结,内存不会立即释放。我建议在切换用户前,主动调用 ActivityManager.killBackgroundProcesses() 来清理一些不重要的进程。
15.5 权限与多用户
每个用户的权限授予状态也是独立的。用户 A 授予了某个 App 定位权限,用户 B 可能没授予。PMS 在检查权限时,会带上 userId 参数:
// PMS 内部权限检查逻辑(简化版)
int checkUidPermission(String permName, int uid) {
int userId = UserHandle.getUserId(uid);
String packageName = getPackageNameForUid(uid);
// 从 mSettings 中获取该用户对该包的权限状态
PermissionsState state = mSettings.getPermissionsState(userId, packageName);
return state.hasPermission(permName) ? PERMISSION_GRANTED : PERMISSION_DENIED;
}
这意味着,同一个 App 在不同用户下,行为可能完全不同。比如用户 A 给了相机权限,App 能拍照;用户 B 没给,App 就只能显示「权限不足」。
注意:系统权限(如 INSTALL_PACKAGES)是全局的,不区分用户。一旦某个 App 被授予了系统权限,它在所有用户下都拥有该权限。这也是为什么系统应用通常不做多用户隔离。
15.6 受限用户与多用户
Android 从 4.3 开始引入了「受限用户」(Restricted Profile)。这种用户不能安装新应用,只能使用管理员已安装的应用。PMS 在处理受限用户时,会额外检查:
- 受限用户只能看到管理员已安装的应用列表
- 受限用户无法调用
PackageInstaller安装新应用 - 受限用户的应用数据目录仍然独立
我记得有一次做儿童模式,就是用受限用户实现的。管理员(家长)安装好学习类 App,然后创建一个受限用户给孩子用。孩子无法安装游戏,也卸载不了学习 App。嗯,这个方案比第三方管控软件靠谱多了。
15.7 多用户下的存储管理
每个用户的数据目录大小不同,PMS 在计算「应用占用空间」时,需要累加所有用户的数据。你可以通过 StorageManager 查询每个用户的存储使用情况:
// 查询用户 10 的存储使用
StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
UserHandle user = UserHandle.of(10);
StorageStats statsForUser = stats.queryStatsForUser(storageUuid, user);
long dataBytes = statsForUser.getDataBytes(); // 该用户的应用数据大小
这里有个坑:getDataBytes() 返回的是该用户下所有 App 的数据总和,但不包括 APK 文件本身。APK 文件大小是全局共享的,不会重复计算。
15.8 多用户与跨用户通信
默认情况下,不同用户的 App 之间不能直接通信。但 Android 提供了 INTERACT_ACROSS_USERS 和 INTERACT_ACROSS_USERS_FULL 权限,允许系统应用跨用户发送广播或启动 Activity。
PMS 在解析 Intent 时,会检查调用者的 userId 和目标组件的 userId 是否一致。如果不一致,且调用者没有跨用户权限,就会抛出 SecurityException。
我建议你在设计多用户 App 时,尽量使用 ContentProvider 配合 android:multiprocess="true" 来实现跨用户数据共享。或者直接用 FileProvider 配合临时权限。
15.9 多用户下的包管理流程图
下面这张图展示了用户 A 安装 App 时,UserManager 和 PMS 的协作流程:
从图中可以看出,UserManager 主要负责「人」的维度——检查当前用户是否有权限安装。PMS 负责「包」的维度——管理 Package 对象和用户数据目录。两者通过 userId 串联起来。
15.10 实战建议
如果你正在开发多用户相关的功能,我有几点建议:
- 测试时多用 adb 模拟多用户:
pm create-user和pm switch-user是调试利器 - 注意文件级加密:Android 7.0+ 默认启用文件级加密,每个用户的数据目录使用不同的密钥。切换用户时,PMS 需要重新挂载解密后的目录
- 不要假设 UID 不变:同一个 App 在不同用户下 UID 不同,但跨用户时 UID 可能复用(如果 App 被卸载又重装)
- 谨慎使用
FLAG_INSTALL_ALL_USERS:这个标志会让 App 安装到所有现有用户,但新创建的用户不会自动安装。需要监听USER_ADDED广播手动处理
个人经验:我曾经在做一个企业设备管理 App 时,需要强制给所有用户安装一个安全策略 App。我用了 PackageInstaller.SessionParams.setInstallFlags() 配合 FLAG_INSTALL_ALL_USERS,结果发现新用户创建后并没有自动安装。后来我注册了 UserManager.USER_ADDED 广播,在新用户创建时主动调用 installExistingPackage() 才解决。
多用户支持是 Android 系统一个非常实用的功能,但实现细节确实不少。理解 UserManager 和 PMS 的协作关系,能帮你避免很多坑。嗯,今天就先聊到这里。