Android多用户功能作为 Android Automotive 的重要组成部分,为不同驾驶员和乘客提供了一个更加定制化、隐私保护的使用环境。本篇博客会详细探讨多用户功能,从基本概念到技术实现,了解Android多用户是怎么实现隔离与共享的平衡。
多用户存在的意义
Android 多用户的存在,它可以让多个用户使用同一台设备,同时保持彼此的数据、应用和设置分隔开来。比如,日常生活中的一些使用场景,比如家庭共享平板、学校中的共享设备、企业环境等。
在车载场景下,也有存在的意义:
在共享车辆、公司车队甚至家庭用车中,不同的驾驶员和乘客常常对车载系统有着不同的偏好。
例如,有人喜欢听音乐,有人更倾向于使用导航;有的人偏爱深色主题,有的人则喜欢简洁的界面。而多用户功能可以让每个用户拥有自己独立的账户,确保设置和数据不会互相影响,从而带来更加个性化和安全的体验。
多用户的支持下,Android Automotive 有以下几点优势:
- 数据隔离:每个用户的数据(包括登录的账户、应用和设置)都是独立的,确保隐私不被泄露。
- 个性化配置:不同用户可以拥有自己独特的界面、应用排序和快捷方式。
- 安全性:通过用户权限管理和数据隔离,避免了敏感数据在用户之间的共享问题。
基础概念
1.UID和UserID的区别
(1)UID
UID(User Identifier) 是 Linux 系统中的用户标识符,Android 基于 Linux 内核运行,也使用 UID 来标识每个进程和应用。
Android 中的 UID 是一个唯一的整数标识符,分配给系统中的每个应用和服务。它不仅用于识别应用,还用于控制应用的权限和进程隔离。
Android 的 UID 分配模式:
- 系统应用:UID 小于 10000 的一般为系统级进程和服务,比如
system用户的 UID 是 1000。 - 普通用户应用:应用安装时,会自动分配一个 10000 以上的 UID。每个用户安装的应用实例都拥有独立的 UID。
- 多用户隔离:Android 将每个用户的应用分配不同的 UID 区间,确保同一个应用在不同用户之间具有独立的 UID,实现用户隔离。
(2)UserID
UserID (用户 ID)是 Android 中多用户功能的标识符,用于区分系统中的不同用户。
每个设备在出厂时的第一个用户(即主用户)拥有 UserID 0,之后创建的新用户则会依次分配新的 UserID,比如 UserID 10、11 等。
在多用户模式下,同一设备的不同用户可以安装相同的应用。为实现隔离,Android 使用不同的 UID 区间,并在每个用户的目录下保存应用数据。UserID 在系统服务中用于识别用户身份并提供用户隔离。
2.UID 与 UserID 的关联
在多用户环境下,每个用户和应用的 UID 是通过 UserID * 100000 + 应用的 UID 进行分配。例如:
- UserID 0 的应用 A 可能拥有 UID 10001。
- UserID 10 的应用 A 会分配到 UID 110001。
这样,即使同一应用在不同用户之间安装,也能通过不同的 UID 实现数据和权限的隔离。
多用户架构设计实现
1.多用户类型与角色
Android 系统的多用户功能主要通过区分不同的用户类型来实现,这些用户类型之间数据完全隔离,权限也有所差异。主要用户类型包括:
- 主用户(Owner User):通常是设备初始化时创建的第一个用户,拥有系统管理员权限,能够管理系统设置、安装或删除应用,并创建或删除其他用户。
- 普通用户(User):普通用户可以安装自己的应用和设置个性化的偏好,但无法进行系统级管理操作,如创建新用户或修改系统设置。
- 访客用户(Guest User):为临时用户而设计,在访客用户退出后,其数据会自动清除,适合短期共享设备的情况。
- 受限用户(Restricted User):通常用于限制特定功能的访问,适合在企业或教育环境中使用。
这些用户类型在 Android Automotive 上有不同的实现,特别是访客模式。每次切换驾驶员时,都可以选择访客模式或已有账户,以确保每位用户的偏好独立且安全。
2. UserManager 服务
UserManager 是 Android 中负责多用户管理的核心系统服务。它负责管理用户的创建、删除、切换以及权限分配
- 创建和删除用户:在设备初始化或主用户操作下创建新的普通用户或访客用户。
- 切换用户:通过
UserManager.switchUser(UserHandle userHandle)切换当前活跃用户,确保不同用户的隔离。 - 用户属性和权限管理:获取用户属性(如用户名称、类型等)以及分配权限。
UserManager 提供了丰富的 API,可以让开发者获取用户信息、管理用户权限和隔离不同用户的数据:
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE); List<UserHandle> users = userManager.getUserProfiles();
UserManager 的实现主要依赖以下几个机制:
- 系统服务的多用户适配:Android 中的很多系统服务(如通知服务、音频服务等)都进行了多用户适配。每个用户可以获得独立的服务实例,确保通知、音量等设置不会在不同用户之间相互影响。
- 权限管理:UserManager 对不同用户分配不同的权限级别。例如,主用户可以管理系统设置和应用,而普通用户仅能使用应用和修改个性化设置。
- 广播与 Intent 隔离:UserManager 还确保每个用户的广播和 Intent 隔离,用户之间的广播不会互相影响,这在应用间通信时尤为重要。
3. UID 隔离与用户空间
在 Android 的多用户系统中,每个用户都有一个独特的用户 ID(UID),并且每个应用在安装时也会获得一个应用特有的 UID。Android 利用 UID 实现了用户级和应用级的进程隔离:
- 用户的 UID 区间:Android 为每个用户分配不同的 UID 区间。例如,系统中主用户(User 0)分配的 UID 区间为
10000-19999,其他用户会依次获得新的 UID 区间。这样,在底层的 Linux 操作系统中,不同用户的进程无法相互访问彼此的资源,因为 Linux 内核默认会阻止不同 UID 间的进程交互。 - 应用的 UID 隔离:在多用户系统中,同一个应用的不同用户实例会拥有不同的 UID。例如,如果两个用户分别安装了同一应用,它们会被赋予不同的 UID,这样即使是同一应用的不同实例,它们之间的数据和进程也无法交互。
用户空间划分主要包括:
- 用户分区:Android 将每个用户的数据存储在
/data/user/路径下,每个用户都有一个独立的文件夹(如/data/user/0对应主用户,/data/user/10对应普通用户)。 - 独立的 App 数据:每个应用的数据在不同用户之间独立存储。例如,
/data/user/0/com.example.app与/data/user/10/com.example.app是完全独立的,互不干扰。
通过这种 UID 和数据分区的隔离机制,即使是同一个应用的不同用户数据也能完全隔离开来。
5. 多用户切换机制
在多用户环境下,系统需要快速切换用户并加载对应的设置和数据。
尤其是在车辆启动或新驾驶员切换时,用户切换需要特别高效,这样可以保证用户拥有流畅的体验。Android 的用户切换流程包含以下几个步骤:
- 保存当前用户状态:在切换前保存当前用户的应用状态、正在运行的进程、屏幕设置等。
- 切换用户数据:挂载新用户的
/data/user分区,加载该用户的应用数据和设置。 - 重启应用:系统会终止当前用户的应用并启动新用户的应用。
- 恢复状态:根据新用户的偏好设置恢复应用、通知等环境。
多用户下的四大组件通信和数据共享
获取当前用户userId的方式
UserHandle中提供myUserId()方法,但是被hide的,可以反射获取:(或者直接根据uid / 100000计算)
private fun readUserIdByReflect(): Int {
var userId = 0
try {
val clz = UserHandle::class.java
val myUserIdMethod = clz.getDeclaredMethod("myUserId")
userId = myUserIdMethod.invoke(null) as Int
} catch (e: Exception) {
}
return userId
}
跨用户启动Activity
Activity/Context提供了startActivityAsUser() 方法,可以传入对应用户的UserHandle来达到跨用户启动Activity的目的
ActivityManager 提供了 startActivityAsUser() 方法,可以在指定用户的上下文中启动 Activity。此方法需要系统权限,如 INTERACT_ACROSS_USERS 或 INTERACT_ACROSS_USERS_FULL,这些权限一般只授予系统应用。
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.targetapp", "com.example.targetapp.TargetActivity"));
// 获取 ActivityManager ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
// 获取目标用户的 UserHandle(假设用户 ID 为 10) UserHandle userHandle = UserHandle.of(10);
// 使用 ActivityManager 跨用户启动 Activity activityManager.startActivityAsUser(intent, userHandle);
可以通过 Context.createPackageContextAsUser(),可以创建一个特定用户的 Context,然后使用这个 Context 来启动该用户的 Activity。这种方法也需要较高权限,适用于系统应用。
// 创建目标用户的 Context(假设用户 ID 为 10)
Context userContext = context.createPackageContextAsUser("com.example.targetapp", 0, UserHandle.of(10));
// 通过用户 Context 启动 Activity
Intent intent = new Intent(userContext, TargetActivity.class);
userContext.startActivity(intent);
在开发调试时,可以使用
adb命令模拟跨用户启动 Activity。例如,假设我们要在用户 ID 为 10 的用户会话中启动 Activity,可以使用以下命令:adb shell am start --user 10 -n com.example.targetapp/.TargetActivity
跨用户启动Service
跨用户启动Service的操作和启动 Activity 一样。受限于系统权限和用户隔离机制。一般情况下,普通应用无法跨用户启动 Service,但系统应用或具有特定权限的应用可以通过特殊方法实现。
Context中提供了startServiceAsUser() 方法
// 创建启动服务的
Intent Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.targetapp", "com.example.targetapp.TargetService"));
// 使用 Context 跨用户启动 Service(假设用户 ID 为 10)context.startServiceAsUser(intent, UserHandle.of(10));
可以使用 ActivityManager 提供的 bindServiceAsUser() 方法来跨用户绑定 Service。这种方法适合需要跨用户绑定 Service 以进行持续通信的场景。
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.targetapp", "com.example.targetapp.TargetService"));
// 使用 bindServiceAsUser 绑定 Service
context.bindServiceAsUser(intent, serviceConnection, Context.BIND_AUTO_CREATE, UserHandle.of(10));
跨用户发送广播
系统应用可以使用 sendBroadcastAsUser() 方法,指定广播的目标用户。这样可以在特定用户的上下文中发送广播。这个方法需要 INTERACT_ACROSS_USERS 或 INTERACT_ACROSS_USERS_FULL 权限。
Intent intent = new Intent("com.example.CUSTOM_BROADCAST");
// 目标用户的 UserHandle,例如用户 ID 为 10
UserHandle targetUserHandle = UserHandle.of(10);
// 发送广播到特定用户
context.sendBroadcastAsUser(intent, targetUserHandle);
sendBroadcastAsUser() 也支持发送有序广播。有序广播会按照优先级顺序在各接收器之间传递,且每个用户的实例会分别处理有序广播,这样可以实现对不同用户的广播控制。
Intent intent = new Intent("com.example.ORDERED_BROADCAST");
UserHandle targetUserHandle = UserHandle.of(10);
// 发送有序广播到特定用户
context.sendOrderedBroadcastAsUser(intent, targetUserHandle, null, null, 0, null, null);
跨用户共享数据Content Provider
系统没有提供类似getContentResolverAsUser的方法,但ContentResolver提供了跨用户query的能力。ContentProvider中提供了maybeAddUserId() 方法。但是被hide了,不能被直接调用。
从源码中可以发现被 query 的 Uri 中可以携带 userId ,所以通过 Uri 中的 userId,可以访问到不同用户下相同 Uri 的 ContentProvider。
// ContentProvider.java
/** @hide */
public static Uri maybeAddUserId(Uri uri, int userId) {
if (uri == null) return null;
if (userId != UserHandle.USER_CURRENT
&& ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
if (!uriHasUserId(uri)) {
//We don't add the user Id if there's already one
Uri.Builder builder = uri.buildUpon();
builder.encodedAuthority("" + userId + "@" + uri.getEncodedAuthority());
return builder.build();
fe }
}
return uri;
}
参考
深入理解Android系统多用户机制