4. 核心数据结构(下):SharedUserSetting、GrantedPermissions、PermissionEntry 源码级解析
好,咱们接着聊。上一节我们把 PackageManager 里几个基础的数据结构过了一遍,像 PackageSetting、PackageParser 这些。今天要讲的这三个——SharedUserSetting、GrantedPermissions、PermissionEntry,说实话,是权限管理和多 APK 共享场景里的核心零件。我个人觉得,不理解它们,你根本没法真正搞懂 Android 的多用户权限模型。
4.1 SharedUserSetting:多 APK 共享同一个 Linux UID 的基石
先问个问题:为什么有些 App 可以访问另一个 App 的数据?比如系统设置里,多个系统 App 能共享文件。答案就在 SharedUserSetting 里。
说白了,SharedUserSetting 就是用来描述「一组共享同一个 Linux UID 的 APK 集合」的数据结构。你想想看,Android 里每个 App 默认都有自己的 UID,但如果你在 AndroidManifest.xml 里声明了 android:sharedUserId,那这些 APK 就会被归到同一个 SharedUserSetting 对象里。
核心源码位置:frameworks/base/services/core/java/com/android/server/pm/SharedUserSetting.java
来看它的关键字段:
// 简化后的 SharedUserSetting 结构
public final class SharedUserSetting extends SettingBase {
final String name; // sharedUserId 字符串
final int userId; // 分配的 Linux UID
final ArrayList<PackageSetting> packages; // 属于该共享 UID 的所有包
int versionFlags; // 版本标记
int pkgFlags; // 包标志位(系统/预置等)
int pkgPrivateFlags; // 私有标志位
}
这里有个细节:packages 是一个 ArrayList,里面存的是所有声明了同一个 sharedUserId 的 PackageSetting 对象。我在项目中遇到过一个问题:两个 APK 声明了相同的 sharedUserId,但签名不一致,结果安装第二个时直接报错。为什么?因为 SharedUserSetting 会校验签名一致性,签名不同就不允许加入同一个共享组。
避坑指南:我曾经在定制系统时,为了省事把两个不同签名的 APK 强行设了同一个 sharedUserId,结果系统直接拒绝安装第二个。后来查源码才发现,SharedUserSetting 在 addPackage() 方法里会调用 signatures.comparingSig() 做签名比对。所以,共享 UID 的前提是签名必须一致。
另外,SharedUserSetting 继承自 SettingBase,这意味着它也有自己的权限集合。嗯,这里要注意:共享 UID 的所有 APK,它们的权限是合并在一起的。也就是说,只要组里有一个 APK 申请了某个权限,整个组的 APK 都能用。这既是便利,也是安全隐患。
4.2 GrantedPermissions:权限授予的「记账本」
接下来这个 GrantedPermissions,名字很直白——就是「已授予的权限」。它是 SettingBase 的父类,SharedUserSetting 和 PackageSetting 都继承自它。
说白了,GrantedPermissions 就是用来记录某个实体(包或共享 UID)当前拥有哪些权限的数据结构。它不关心权限是怎么来的(是安装时授予的,还是运行时动态申请的),它只负责记账。
// 简化后的 GrantedPermissions 结构
public class GrantedPermissions {
final String[] grantedPermissions; // 已授予的权限名数组
final int[] gids; // 对应的 GID 数组
final int[] permissionFlags; // 每个权限的标志位
int pkgFlags; // 包标志
int pkgPrivateFlags; // 私有标志
}
这里有个设计上的巧妙之处:grantedPermissions 是一个 String 数组,而不是 Set 或 Map。为什么用数组?我个人猜测是为了序列化和跨进程传递方便。毕竟 PackageManagerService 经常需要把这些数据写到 XML 里持久化,数组的序列化开销比集合小得多。
再看 gids 字段。每个 Android 权限都对应一个或多个 GID(组 ID)。当系统检查权限时,实际上是通过检查进程的 GID 集合来判断的。比如 android.permission.INTERNET 对应 GID 3003,如果进程的 GID 集合里包含 3003,就说明它有网络权限。
小技巧:我在调试权限问题时,经常用 adb shell ps | grep <包名> 查看进程的 GID,然后对照 /system/etc/permissions/platform.xml 里的权限-GID 映射表,能快速定位权限缺失的原因。
还有一个字段 permissionFlags,它记录每个权限的额外状态,比如:
FLAG_PERMISSION_GRANTED:已授予FLAG_PERMISSION_USER_SET:用户手动设置过FLAG_PERMISSION_REVOKED:已被撤销FLAG_PERMISSION_POLICY_FIXED:策略固定,用户不能改
这些标志位在运行时权限管理中非常关键。比如 FLAG_PERMISSION_POLICY_FIXED,企业设备管理员可以通过 DevicePolicyManager 把某些权限固定为拒绝,用户自己在设置里都改不了。
4.3 PermissionEntry:权限的「身份证」
最后这个 PermissionEntry,相对简单一些,但同样重要。它代表一个具体的权限条目,包含了权限的所有元数据。
// 简化后的 PermissionEntry 结构
public final class PermissionEntry {
final String name; // 权限名,如 android.permission.CAMERA
final int type; // 权限类型(普通/危险/签名等)
final int protectionLevel; // 保护级别
final String group; // 所属权限组
final String description; // 描述
final int requestRes; // 请求时的资源 ID
final int descriptionRes; // 描述的资源 ID
final boolean runtimeOnly; // 是否仅运行时使用
}
你可能会问:这个 PermissionEntry 和前面讲的 GrantedPermissions 里的权限名有什么区别?区别大了。GrantedPermissions 里的权限名只是一个字符串,而 PermissionEntry 包含了这个权限的完整定义。打个比方:GrantedPermissions 是「张三有驾照」这个事实,PermissionEntry 是「驾照」这个证件本身——它规定了驾照的等级、有效期、准驾车型等。
PermissionEntry 是在系统启动时,从 /system/etc/permissions/ 目录下的 XML 文件里解析出来的。每个 XML 文件定义一组权限,比如 platform.xml 定义了平台权限,com.google.android.xxx.xml 定义了 Google 服务的权限。
关键点:PermissionEntry 的 protectionLevel 字段决定了权限的授予方式。比如 dangerous 级别的权限需要运行时弹窗请求,signature 级别的权限只有同签名的 App 才能获取。这个字段在 Android 6.0 引入运行时权限后变得尤为重要。
我记得有一次,客户反馈说某个预装 App 的权限在设置里显示为「已拒绝」,但 App 功能却正常。查了半天发现,这个权限的 protectionLevel 是 signature|privileged,而 App 是系统预装且有系统签名,所以系统直接授予了,根本不需要用户同意。嗯,这就是 protectionLevel 的威力。
4.4 三者之间的关系:一张图说清楚
这三个数据结构不是孤立的,它们共同构成了 Android 权限管理的核心骨架。我画了一张图,帮你理清它们之间的关系:
从这张图可以看出:
- SharedUserSetting 和 PackageSetting 都继承自 GrantedPermissions,所以它们都有权限授予记录。
- PermissionEntry 是权限的「定义」,它描述了权限的属性,但不记录谁拥有它。
- 多个 PackageSetting 可以通过 SharedUserSetting 共享同一个 UID 和权限集合。
4.5 实战:如何通过源码定位权限问题
讲完理论,来点实际的。假设你遇到一个 Bug:某个 App 明明在 AndroidManifest 里声明了权限,但运行时却拿不到。你会怎么查?
我的排查思路是这样的:
- 先看 PermissionEntry:检查这个权限的 protectionLevel 是不是
signature或system,如果是,App 的签名或系统权限可能不满足。 - 再看 GrantedPermissions:用
dumpsys package <包名>查看grantedPermissions列表,确认权限是否真的被授予了。 - 最后看 SharedUserSetting:如果 App 声明了 sharedUserId,检查它所属的共享组里其他 App 的权限状态。我曾经遇到一个情况:共享组里有个 App 被卸载了,导致整个组的权限状态异常。
实用命令:
adb shell dumpsys package <包名>— 查看包的权限授予情况adb shell dumpsys package packages | grep sharedUser— 查看所有共享 UID 组adb shell pm list permissions -g— 列出所有权限组及其权限
嗯,这三个数据结构虽然看起来不起眼,但它们是整个权限系统的地基。理解了它们,你再看 PackageManagerService 里那些几百行的权限检查逻辑,就不会觉得晕了。
好了,这一节就到这里。记住:SharedUserSetting 管「谁和谁是一家」,GrantedPermissions 管「家里有什么权限」,PermissionEntry 管「权限长什么样」。三者配合,构成了 Android 权限管理的完整图景。
公众号:蓝海资料掘金营,微信deep3321