11. 权限管理机制(上):权限定义、权限请求、运行时权限的底层实现

权限管理,说白了就是Android系统的一道门禁。你想想看,一个App凭什么能读你的通讯录?凭什么能偷偷录音?这背后全靠权限机制在撑着。

我刚开始做Android开发那会儿,还是API Level 8的时代。那时候权限只有一种——安装时授权。用户要么全给,要么别装。后来Google发现这样太粗暴了,于是在Android 6.0引入了运行时权限。嗯,今天我们就来扒一扒这套机制的底层实现。

11.1 权限的定义与分类

权限的定义,本质上就是一个字符串声明。每个权限都有一个唯一的名称,比如 android.permission.CAMERA。系统通过这个字符串来标识和检查权限。

权限分三类,我习惯用颜色来记忆:

权限类型 授权方式 典型例子
普通权限 (Normal) 安装时自动授予 INTERNET、ACCESS_NETWORK_STATE
危险权限 (Dangerous) 运行时弹窗请求 CAMERA、READ_CONTACTS
签名权限 (Signature) 同签名自动授予 BIND_ACCESSIBILITY_SERVICE

核心要点:危险权限是运行时权限的主角。普通权限系统静默授予,签名权限只有相同签名的App才能获取。

我在项目中遇到过一个问题:某个App声明了 READ_PHONE_STATE 权限,但在Android 10上始终拿不到IMEI。后来才发现,Google从Android 10开始把这个权限从危险降级成了普通权限,而且IMEI的读取还需要额外的 READ_PRIVILEGED_PHONE_STATE 签名权限。嗯,权限的等级是会变的。

11.2 权限请求的完整流程

一个App请求运行时权限,底层到底发生了什么?我画了一张图,你看完就明白了。

运行时权限请求底层流程 App调用 requestPermissions() ActivityManagerService 处理 PackageManagerService 检查 权限已授予 → 直接回调 权限未授予 → 弹窗询问 用户选择 → 回调 onRequestPermissionsResult()

流程其实不复杂。App调用 requestPermissions() 后,请求会通过Binder跨进程跑到 ActivityManagerService。AMS再转交给 PackageManagerService 做权限检查。

PMS会查三样东西:

  1. 权限是否声明——AndroidManifest里有没有写?
  2. 权限是否已授予——用户之前给过没?
  3. 权限的授权方式——是普通还是危险?

如果权限已经授予,直接回调 onRequestPermissionsResult() 返回 PERMISSION_GRANTED。如果没授予,系统会弹出一个Dialog,让用户选择。

小技巧:我个人习惯在 onRequestPermissionsResult() 里判断 shouldShowRequestPermissionRationale() 的返回值。如果返回false,说明用户勾选了"不再询问",这时候需要引导用户去设置里手动开启。

11.3 运行时权限的底层实现

运行时权限的底层,说白了就是一套基于UID的权限检查机制。每个App安装时,系统会分配一个唯一的UID。PMS维护着一张权限表,记录着每个UID拥有哪些权限。

关键代码在 PackageManagerService.java 里:

// 简化的权限检查逻辑
public int checkUidPermission(String permName, int uid) {
    // 1. 获取UID对应的包名列表
    String[] pkgNames = getPackagesForUid(uid);
    
    // 2. 遍历每个包,检查权限
    for (String pkgName : pkgNames) {
        PackageSetting ps = mSettings.getPackageLPr(pkgName);
        if (ps != null) {
            // 3. 查权限授予状态
            if (ps.getPermissionsState()
                  .hasPermission(permName)) {
                return PERMISSION_GRANTED;
            }
        }
    }
    return PERMISSION_DENIED;
}

这段代码看着简单,但背后藏着不少细节。我踩过一个坑:checkUidPermission() 检查的是 静态授予的权限,也就是安装时或通过adb授予的权限。运行时权限的检查走的是另一条路——RuntimePermissionPresenter

运行时权限的授予状态存在 /data/system/users/0/runtime-permissions.xml 里。每次用户授权或撤销,PMS都会更新这个文件。你想想看,如果频繁读写这个文件,性能会不会有问题?

嗯,Google当然想到了。PMS内部维护了一个 RuntimePermissionsState 的内存缓存。只有缓存和文件不一致时,才会触发写操作。这个设计挺巧妙的。

11.4 权限组与权限的对应关系

危险权限是按组管理的。比如 CAMERA 权限自己就是一个组,而 READ_CONTACTSWRITE_CONTACTS 属于同一个 CONTACTS 组。

为什么要分组?因为用户体验。用户授权了 READ_CONTACTS,系统会默认把同组的 WRITE_CONTACTS 也一起授权。反过来,用户撤销了 READ_CONTACTS,同组的 WRITE_CONTACTS 也会被撤销。

权限组 包含的权限
CALENDAR READ_CALENDAR, WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION
PHONE READ_PHONE_STATE, CALL_PHONE, READ_CALL_LOG, WRITE_CALL_LOG, ADD_VOICEMAIL, USE_SIP, PROCESS_OUTGOING_CALLS
SMS SEND_SMS, RECEIVE_SMS, READ_SMS, RECEIVE_WAP_PUSH, RECEIVE_MMS
STORAGE READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, ACCESS_MEDIA_LOCATION

注意:从Android 11开始,ACCESS_BACKGROUND_LOCATION 被单独拎出来了。即使用户授权了前台定位,后台定位还需要单独申请。我曾经有个项目因为这个改动,定位功能在后台直接挂了,排查了半天才发现是权限问题。

11.5 权限检查的Binder调用链

权限检查不是App自己说了算的。每次App调用一个需要权限的API,比如 Camera.open(),底层都会触发一次Binder调用。

调用链大概是这样的:

App进程 → CameraService (native) → ServiceManager → 
ActivityManagerService → PackageManagerService → 
检查UID权限 → 返回结果

你可能会问:为什么绕这么大一圈?直接让App自己检查不行吗?

当然不行。你想想看,如果App能自己检查权限,那恶意App完全可以绕过检查,直接调用底层API。系统必须确保 每一次 敏感操作都经过系统服务的验证。

我记得有一次调试一个奇怪的问题:某个App在Android 9上能正常拍照,升级到Android 10后却闪退。抓了log才发现,是 Camera.open() 返回了 Permission Denial。原因是App在后台请求了相机权限,而Android 10对后台权限的限制更严格了。

11.6 权限的持久化与恢复

权限授予后不是永久不变的。系统升级、App更新、甚至用户清除数据,都可能影响权限状态。

PMS在 /data/system/users/0/ 目录下维护着几个关键文件:

  • runtime-permissions.xml — 运行时权限的授予状态
  • packages.xml — 所有已安装App的权限声明
  • packages.list — App的UID和包名映射

系统启动时,PMS会读取这些文件,重建内存中的权限状态。如果文件损坏或丢失,系统会重新扫描所有已安装的App,重新生成权限数据。

避坑指南:我曾经遇到过OTA升级后,部分App的运行时权限全部丢失的情况。原因是升级脚本没有正确处理权限迁移。如果你在做系统升级相关的开发,一定要测试权限数据的兼容性。

嗯,权限管理机制的上半部分就到这里。我们聊了权限的定义、分类、请求流程、底层实现,还有权限组和Binder调用链。这些东西看着零散,但串起来就是一套完整的权限管理体系。

下一部分我们会深入权限的授予策略、权限迁移、以及Android 11/12/13的新变化。到时候再聊。


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