26、多用户管理:多用户架构、用户切换、Profile 管理、数据隔离

多用户管理,说白了就是让一辆车能服务好一家人。你想想看,爸爸开车时导航偏好是“避开拥堵”,妈妈上车后却想走“最短距离”,孩子坐后排只想听儿歌。如果没有多用户机制,每次换人开车都得手动改一遍设置,体验太差了。

Android Automotive OS 的多用户架构,本质上是从 Android 手机的多用户功能演变过来的。但车载场景有个特殊之处——用户切换不能像手机那样频繁重启进程,得做到“无缝切换”。嗯,这里面的门道不少,我一个个讲。

多用户架构:谁是谁的“主人”?

在 Android Automotive 里,用户分两种:系统用户普通用户。系统用户(User 0)是开机后第一个启动的用户,它负责管理所有系统服务、硬件访问权限。普通用户则是驾驶员或乘客的账号。

我习惯把系统用户比作“酒店前台”,普通用户是“住客”。前台负责分配房间钥匙、管理水电,但前台自己不住房间。普通用户的数据、设置、应用安装状态,彼此完全隔离。

来看一张架构图,帮你快速理解用户之间的关系:

Android Automotive OS 多用户架构 系统用户 (User 0) 系统服务 · 硬件管理 · 权限控制 用户 A(驾驶员) 导航偏好 · 座椅记忆 音乐账号 · 通话记录 用户 B(家庭成员) 导航偏好 · 座椅记忆 音乐账号 · 通话记录 用户 C(访客) 临时账号 · 无持久数据 受限应用访问 数据隔离层 每个用户拥有独立的 /data/user/{userId} 目录 ContentProvider 跨用户访问需显式授权

每个普通用户都有独立的 /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);
}
⚠️ 注意:切换用户时,当前用户的所有前台 Activity 会被销毁。如果你的应用有未保存的数据,记得在 onSaveInstanceState() 中做好持久化。我曾经见过一个导航应用,切换用户后路线丢失,用户差点投诉到消协。

切换过程中,系统会依次执行:

  1. 停止当前用户的前台进程(发送 ACTION_USER_BACKGROUND 广播)
  2. 启动目标用户的前台进程(发送 ACTION_USER_FOREGROUND 广播)
  3. 更新 SystemUI 的状态栏、导航栏显示
  4. 恢复目标用户的应用启动状态

这里有个坑:广播接收是异步的。如果你的应用依赖 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", "创建失败,可能是已达到用户数量上限");
}
💡 小技巧:车载系统通常限制最多 8 个用户(包括 Profile)。别贪多,每个用户都会占用内存和存储空间。我建议普通用户不超过 4 个,Profile 不超过 2 个。

Profile 的数据隔离比普通用户更严格。普通用户之间可以通过 ContentProvidergrantUriPermission 共享数据,但 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 声明,八成能找到原因。


公众号:蓝海资料掘金营,微信deep3321