26、多用户管理:多用户架构、用户切换、Profile 管理、数据隔离
多用户管理,说白了就是让一辆车能服务好一家人。你想想看,爸爸开车时导航偏好是“避开拥堵”,妈妈上车后却想走“最短距离”,孩子坐后排只想听儿歌。如果没有多用户机制,每次换人开车都得手动改一遍设置,体验太差了。
Android Automotive OS 的多用户架构,本质上是从 Android 手机的多用户功能演变过来的。但车载场景有个特殊之处——用户切换不能像手机那样频繁重启进程,得做到“无缝切换”。嗯,这里面的门道不少,我一个个讲。
多用户架构:谁是谁的“主人”?
在 Android Automotive 里,用户分两种:系统用户和普通用户。系统用户(User 0)是开机后第一个启动的用户,它负责管理所有系统服务、硬件访问权限。普通用户则是驾驶员或乘客的账号。
我习惯把系统用户比作“酒店前台”,普通用户是“住客”。前台负责分配房间钥匙、管理水电,但前台自己不住房间。普通用户的数据、设置、应用安装状态,彼此完全隔离。
来看一张架构图,帮你快速理解用户之间的关系:
每个普通用户都有独立的 /data/user/{userId} 目录。应用安装状态也是独立的——用户 A 装了 Spotify,用户 B 可能没装。这在车载场景下非常实用:驾驶员只需要导航和电话,后排乘客可能更需要娱乐应用。
用户切换:不是简单的“换账号”
用户切换在手机上可能只是锁屏界面换个头像,但在车上,切换用户意味着整个 HMI(人机交互界面)要重新加载。我参与的第一个车载项目,用户切换时仪表盘和中控屏会黑屏 3 秒,被客户骂惨了。
后来我们优化了流程。核心思路是:预创建用户进程。当系统检测到驾驶员接近车辆(通过蓝牙钥匙或 NFC),就在后台提前创建好该用户的进程。等用户真正上车,切换时只需要把焦点从当前用户进程转移到新用户进程,几乎无感。
代码层面,用户切换的核心 API 是 UserManager:
// 获取 UserManager 实例
UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
// 切换到指定用户(需要 MANAGE_USERS 权限)
UserHandle targetUser = UserHandle.of(userId);
try {
userManager.switchUser(targetUser);
} catch (SecurityException e) {
Log.e("UserSwitch", "没有切换用户的权限", e);
}
切换过程中,系统会依次执行:
- 停止当前用户的前台进程(发送
ACTION_USER_BACKGROUND广播) - 启动目标用户的前台进程(发送
ACTION_USER_FOREGROUND广播) - 更新 SystemUI 的状态栏、导航栏显示
- 恢复目标用户的应用启动状态
这里有个坑:广播接收是异步的。如果你的应用依赖 ACTION_USER_FOREGROUND 来做初始化,别假设它会在 switchUser() 返回前执行完。我习惯在广播接收器里加一个 CountDownLatch 来同步,但要注意超时时间别设太长,否则会影响切换速度。
Profile 管理:工作与生活的边界
Android Automotive 支持 Profile 概念。Profile 是用户下的子账号,典型场景是“访客模式”或“代客泊车模式”。
举个例子:你把车送去保养,维修师傅需要开你的车出去试驾。你肯定不想让他看到你的通话记录、导航历史。这时候就可以创建一个“代客 Profile”,限制只能访问导航和音乐,不能查看联系人。
创建 Profile 的代码示例:
// 创建托管 Profile(需要 MANAGE_USERS 权限)
UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
UserHandle parentUser = Process.myUserHandle();
// 创建 Profile
UserHandle profile = userManager.createProfileForUser(
"代客模式",
UserManager.USER_TYPE_PROFILE_MANAGED,
UserHandle.myUserId()
);
if (profile != null) {
Log.i("Profile", "代客模式创建成功,userId: " + profile.getIdentifier());
} else {
Log.e("Profile", "创建失败,可能是已达到用户数量上限");
}
Profile 的数据隔离比普通用户更严格。普通用户之间可以通过 ContentProvider 的 grantUriPermission 共享数据,但 Profile 默认禁止任何跨用户数据访问。这是为了防止恶意应用通过 Profile 窃取主用户的数据。
数据隔离:不只是“分文件夹”
数据隔离听起来简单——每个用户一个文件夹嘛。但实际落地时,有几个地方特别容易出问题。
第一,SharedPreferences 的隔离。 每个用户有自己的 /data/data/{package}/shared_prefs/ 目录。但如果你用 Context.createPackageContext() 访问其他用户的上下文,系统会抛异常。我见过一个团队在写日志工具时,用了 getSharedPreferences() 但没指定用户,结果所有用户的日志混在一起,排查问题时根本分不清是谁的操作。
第二,ContentProvider 的跨用户访问。 默认情况下,ContentProvider 只能被同一用户的应用访问。如果你需要跨用户共享数据(比如让所有用户都能看到“车辆设置”),必须在 Manifest 中声明:
<provider
android:name=".VehicleSettingsProvider"
android:authorities="com.example.vehicle.settings"
android:exported="true"
android:singleUser="true">
</provider>
singleUser="true" 表示这个 Provider 只会在系统用户(User 0)中运行,所有用户都通过系统用户来访问它。这样数据就统一了。
第三,文件存储的隔离。 每个用户的 getExternalFilesDir() 返回的路径不同。但如果你用 Environment.getExternalStorageDirectory(),它指向的是公共存储区域,所有用户都能读写。嗯,这里要小心——不要把敏感数据放到公共区域。
| 存储类型 | 隔离级别 | 适用场景 |
|---|---|---|
| /data/data/{package}/ | 完全隔离 | 用户私有数据、账号 Token |
| getExternalFilesDir() | 按用户隔离 | 下载的离线地图、音乐缓存 |
| Environment.getExternalStorageDirectory() | 全局共享 | 系统日志、OTA 升级包 |
| ContentProvider (singleUser) | 跨用户共享 | 车辆设置、驾驶模式 |
核心原则:能隔离的尽量隔离,必须共享的才共享。我在项目中定过一个规矩——所有新功能的数据存储方案,必须先过“数据隔离评审”,否则不准合入主干。这规矩救了我们好几次,避免了很多隐私合规问题。
避坑指南:我曾经踩过的三个坑
坑一:用户切换时 Notification 的归属问题。 用户 A 在后台播放音乐,用户 B 切换上来后,音乐还在响,但通知栏显示的是用户 B 的通知。用户 B 想关掉音乐,却找不到控制入口。解决方案:在 onUserSwitchComplete() 回调中,主动清理不属于当前用户的通知。
坑二:多用户下的蓝牙配对。 用户 A 配对了手机,用户 B 切换上来后,蓝牙会自动断开。但有些车机不会自动重连用户 B 的手机。我当时的做法是:在用户切换完成后,主动触发蓝牙扫描,匹配该用户历史配对的设备。
坑三:应用安装状态不一致。 用户 A 安装了某个应用,用户 B 切换上来后发现应用图标在桌面上,但点击后提示“应用未安装”。这是因为应用安装是按用户隔离的。解决方案:在桌面启动器中,只显示当前用户已安装的应用。这个逻辑看起来简单,但很多第三方启动器都没处理好。
多用户管理,说白了就是“分而治之”。把每个用户的数据、设置、应用状态都管好,切换时做到无缝,用户才会觉得这车“懂我”。如果你在实现过程中遇到奇怪的问题,多半是数据隔离没做好——回去检查一下你的文件路径和 ContentProvider 声明,八成能找到原因。