47、多用户调试:UserManager、工作资料、多用户切换、应用隔离
多用户调试,说实话,在普通App开发里接触得不多。但一旦你开始做企业级应用、MDM(移动设备管理)或者系统级开发,这块就是绕不开的坎。
我记得刚接手一个项目,客户反馈说“App在手机上是好的,但在工作资料里就崩了”。我一开始还以为是权限问题,查了半天才发现——多用户环境下的数据隔离和组件调用,跟单用户完全是两码事。
今天我们就来聊聊,Android多用户调试到底怎么搞。
1. 多用户架构:Android是怎么隔离用户的?
Android从4.2开始支持多用户,到了5.0引入了工作资料(Work Profile)。说白了,每个用户就是一个独立的“空间”,有自己的UID、存储目录、应用实例。
我画了一张图,帮你快速理解这个结构:
看到没?User 0是主用户,User 10是工作资料,User 11可能是访客。每个用户都有自己的/data/user/{userId}目录,应用也是独立安装的。
核心要点:多用户调试,本质上就是搞清楚“当前代码跑在哪个用户空间里”。
2. UserManager:多用户管理的核心API
UserManager是Android多用户管理的入口。我习惯用这个类来获取当前用户信息、创建用户、切换用户等。
先看几个最常用的API:
// 获取UserManager实例
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
// 获取当前用户
UserHandle userHandle = android.os.Process.myUserHandle();
int userId = UserHandle.myUserId();
// 获取用户序列号(唯一标识)
long serialNumber = um.getSerialNumberForUser(userHandle);
// 判断当前用户是不是主用户
boolean isSystemUser = um.isSystemUser();
// 判断是不是工作资料
boolean isManagedProfile = um.isManagedProfile();
// 获取所有用户(包括已解锁的)
List<UserHandle> userProfiles = um.getUserProfiles();
我在项目中遇到过一个问题:getUserProfiles()返回的列表里,可能包含未解锁的用户。如果你直接去访问那个用户的ContentProvider,就会抛出SecurityException。所以调用前一定要先检查用户状态。
调试技巧:用adb shell dumpsys user可以快速查看当前设备上的所有用户状态。
3. 工作资料调试:那些年踩过的坑
工作资料(Managed Profile)是企业级应用最常见的场景。说白了,就是在手机上划出一个“工作区”,跟个人空间完全隔离。
调试工作资料时,有几个点特别容易出问题:
3.1 跨用户调用
工作资料里的App想访问主用户的数据?不行。反过来也不行。除非你用了跨用户ContentProvider。
// 跨用户ContentProvider的调用方式
Uri uri = Uri.parse("content://com.example.provider/data");
// 关键:加上UserHandle
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(uri, null, null, null, null);
// 但如果你要跨用户查询,需要这样:
UserHandle workUser = ...; // 获取工作资料的用户句柄
ContentResolver resolverForUser = context.getContentResolver();
// 注意:跨用户查询需要INTERACT_ACROSS_USERS权限
resolverForUser.acquireUnstableProviderOrThrow(uri);
我曾经调试过一个Bug:工作资料里的App启动后,一直报“Calling from user 10 but provider is in user 0”。查了半天才发现,ContentProvider的android:authorities在Manifest里写错了,导致跨用户路由失败。
警告:跨用户操作需要声明INTERACT_ACROSS_USERS或INTERACT_ACROSS_USERS_FULL权限。但这两个权限是系统级权限,普通App拿不到。如果你在做系统App,记得在AndroidManifest里加上。
3.2 工作资料的安装与卸载
工作资料里的应用安装,跟主用户是独立的。你想想看,同一个App在主用户里装了,工作资料里不一定有。
// 通过PackageManager安装应用到指定用户
PackageManager pm = context.getPackageManager();
// 安装到用户10(工作资料)
pm.installExistingPackage("com.example.app", PackageManager.INSTALL_REASON_USER, new UserHandle(10));
// 卸载指定用户的应用
pm.deletePackageAsUser("com.example.app", null, PackageManager.DELETE_ALL_USERS, new UserHandle(10));
嗯,这里要注意:installExistingPackage只能安装已经存在于设备上的包。如果你想全新安装,得用PackageInstaller,那又是另一套API了。
4. 多用户切换:调试时怎么切用户?
调试多用户切换,最直接的方式就是用adb命令:
# 查看当前用户
adb shell am get-current-user
# 切换用户(需要系统权限)
adb shell am switch-user 10
# 创建新用户
adb shell pm create-user "测试用户"
# 删除用户
adb shell pm remove-user 10
# 启动工作资料设置
adb shell am start -a android.settings.MANAGE_PROFILE_SETTINGS
我个人习惯在调试时,同时打开两个logcat窗口:
# 窗口1:过滤当前用户日志
adb logcat --uid=10010
# 窗口2:查看系统用户切换事件
adb logcat -s ActivityManager:V *:S
这样能同时看到应用日志和系统切换事件,定位问题快很多。
5. 应用隔离:数据、进程、权限
多用户下的应用隔离,主要体现在三个方面:
| 隔离维度 | 说明 | 调试要点 |
|---|---|---|
| 数据隔离 | 每个用户有独立的/data/data/{package}目录 | 用adb shell run-as {package} --user 10查看 |
| 进程隔离 | 同一App在不同用户下是不同进程,UID不同 | ps -A | grep {package} 看UID |
| 权限隔离 | 权限授予是用户级别的 | dumpsys package {package} 看grantedPermissions |
举个例子:你在主用户里给了App定位权限,工作资料里的同一App并不会自动获得。需要单独授权。
// 检查指定用户的权限状态
PackageManager pm = context.getPackageManager();
int permissionState = pm.checkPermission(
"android.permission.ACCESS_FINE_LOCATION",
"com.example.app",
new UserHandle(10)
);
if (permissionState == PackageManager.PERMISSION_GRANTED) {
// 工作资料里已经有权限了
} else {
// 需要重新申请
}
6. 调试实战:一个典型的多用户问题排查
我记得有一次,客户反馈说“工作资料里的App收不到推送”。我排查的步骤是这样的:
- 确认用户状态:adb shell dumpsys user,看到User 10是RUNNING_UNLOCKED状态,没问题。
- 检查进程:ps -A | grep com.example.push,发现User 10下根本没有这个进程。说明App没启动。
- 查看广播接收器:dumpsys activity broadcasts | grep com.example.push,发现广播接收器只注册在User 0下。
- 定位根因:原来推送SDK在初始化时,只注册了主用户的广播接收器。工作资料里的App启动后,没有重新注册。
解决方案很简单:在Application.onCreate()里,判断当前用户是不是工作资料,如果是,就重新注册广播接收器。
@Override
public void onCreate() {
super.onCreate();
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
if (um.isManagedProfile()) {
// 工作资料环境,重新注册推送
registerPushReceiver();
}
}
我的建议:多用户调试时,一定要养成“先看用户ID”的习惯。很多问题其实不是代码逻辑错了,而是跑错了用户空间。
7. 总结一下
多用户调试,说白了就是搞清楚三件事:
- 当前代码在哪个用户里跑?——用UserHandle.myUserId()
- 目标资源在哪个用户里?——用UserManager.getUserProfiles()
- 跨用户操作有没有权限?——检查INTERACT_ACROSS_USERS
嗯,掌握了这三点,多用户调试就不再是玄学了。你想想看,Android的多用户机制其实挺优雅的——每个用户一个独立空间,互不干扰。但调试的时候,就得时刻记住“我在哪个空间里”。
希望今天的分享对你有帮助。如果你在实际项目中遇到什么奇葩的多用户问题,欢迎来公众号找我聊聊。
公众号:蓝海资料掘金营,微信deep3321