47、多用户调试:UserManager、工作资料、多用户切换、应用隔离

多用户调试,说实话,在普通App开发里接触得不多。但一旦你开始做企业级应用、MDM(移动设备管理)或者系统级开发,这块就是绕不开的坎。

我记得刚接手一个项目,客户反馈说“App在手机上是好的,但在工作资料里就崩了”。我一开始还以为是权限问题,查了半天才发现——多用户环境下的数据隔离和组件调用,跟单用户完全是两码事

今天我们就来聊聊,Android多用户调试到底怎么搞。

1. 多用户架构:Android是怎么隔离用户的?

Android从4.2开始支持多用户,到了5.0引入了工作资料(Work Profile)。说白了,每个用户就是一个独立的“空间”,有自己的UID、存储目录、应用实例。

我画了一张图,帮你快速理解这个结构:

Android 多用户架构示意图 System User (User 0) — 系统服务、AMS、PMS、WMS User 0(主用户) UID: 10000 /data/user/0/ 应用实例 A, B, C User 10(工作资料) UID: 10010 /data/user/10/ 应用实例 A', B', C' User 11(访客) UID: 10011 /data/user/11/ 受限应用集 每个用户拥有独立的 /data/user/{userId} 目录 应用在不同用户下是独立进程,UID不同 系统服务通过 userId 区分调用来源

看到没?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_USERSINTERACT_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收不到推送”。我排查的步骤是这样的:

  1. 确认用户状态:adb shell dumpsys user,看到User 10是RUNNING_UNLOCKED状态,没问题。
  2. 检查进程:ps -A | grep com.example.push,发现User 10下根本没有这个进程。说明App没启动。
  3. 查看广播接收器:dumpsys activity broadcasts | grep com.example.push,发现广播接收器只注册在User 0下。
  4. 定位根因:原来推送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