33、权限系统调试:权限声明、运行时权限、权限组、SELinux上下文

权限问题,是Android调试里最让人头疼的几类问题之一。我见过太多开发者在权限上栽跟头——明明清单里写了,运行时也弹窗了,可功能就是跑不起来。说白了,Android的权限系统是个多层过滤网,你只打通一层是不够的。

今天咱们就把这四层全捋一遍:权限声明、运行时权限、权限组、SELinux上下文。每一层都可能卡住你。

33.1 权限声明:第一道门槛

权限声明写在AndroidManifest.xml里。这是最基础的一步,但也是最容易被忽略的。

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />

这里有个坑:从Android 12开始,部分权限的声明方式变了。比如BLUETOOTH_SCANBLUETOOTH_CONNECT这些新权限,不能再用旧的方式声明。我去年帮一个团队排查蓝牙连接失败的问题,查了半天才发现是权限声明格式不对。

注意:Android 14(API 34)引入了READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIO三个细粒度权限,替代了READ_EXTERNAL_STORAGE。如果你的targetSdkVersion >= 34,必须使用新权限。

33.2 运行时权限:动态申请的逻辑

Android 6.0(API 23)之后,危险权限需要运行时申请。这个流程大家应该都熟悉:检查权限 → 请求权限 → 处理回调。

// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    // 请求权限
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.CAMERA}, REQUEST_CODE);
} else {
    // 已有权限,直接操作
    openCamera();
}

但调试时真正让人头疼的是权限被永久拒绝的情况。用户点了"不再询问"之后,你再弹窗也没用了。这时候只能引导用户去设置里手动开启。

调试技巧:adb shell dumpsys package <包名>可以查看当前应用的权限授予状态。我经常用这个命令快速确认权限是否真的被授予了,比看代码log更直接。

33.3 权限组:别被名字骗了

权限组(Permission Group)是个容易混淆的概念。同一个组里的权限,只要用户授权了一个,系统会自动授权同组的其他权限。但要注意:这只适用于运行时权限的首次授权

权限组 包含的权限
STORAGE READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE
LOCATION ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
CAMERA CAMERA
MICROPHONE RECORD_AUDIO

我曾经遇到过一个场景:用户授权了ACCESS_FINE_LOCATION,但我的代码里同时请求了ACCESS_COARSE_LOCATION。我以为同组权限会自动授权,结果发现checkSelfPermission返回的是PERMISSION_DENIED。为什么?因为权限组的自动授权只在请求权限的弹窗流程中生效,如果你单独检查某个权限,系统不会自动帮你补上。

33.4 SELinux上下文:最隐蔽的拦路虎

SELinux(Security-Enhanced Linux)是Android安全模型的最后一道防线。即使你的应用有了所有权限,SELinux策略仍然可能阻止你访问某些资源。

调试SELinux问题,最常用的命令是dmesglogcat。当SELinux拒绝访问时,内核会打印类似这样的日志:

avc: denied { read } for pid=1234 comm="myapp" name="secret_file" 
    scontext=u:r:untrusted_app:s0:c512,c768 
    tcontext=u:object_r:system_data_file:s0 tclass=file permissive=0

这条日志告诉我们:untrusted_app域的应用试图读取system_data_file类型的文件,被拒绝了。解决方案要么修改SELinux策略(需要系统签名),要么换个路径存放文件。

核心思路:SELinux调试三板斧——
1. 看avc: denied日志,确认拒绝的上下文
2. 用ls -Z查看文件/进程的SELinux标签
3. 用setenforce 0临时关闭SELinux(仅限eng/userdebug版本)来验证问题是否由SELinux引起

我个人习惯在调试阶段先setenforce 0,如果问题消失,那基本就是SELinux策略的问题。但注意:生产环境绝对不能这么做,这会让设备处于不安全状态。

33.5 四层权限的调试流程图

下面这张图总结了权限调试的完整路径。你可以把它贴在工位旁边,遇到权限问题就按这个流程排查。

Android权限调试四层模型 权限声明 AndroidManifest.xml 运行时权限 动态申请 + 回调处理 权限组 同组自动授权 SELinux 上下文 avc: denied 日志 → 检查 scontext / tcontext → 修改策略或调整路径 调试工具速查 🔍 dumpsys package 🔍 logcat | grep avc 🔍 ls -Z /path/to/file 🔍 setenforce 0/1 🔍 getenforce 🔍 pm grant/revoke

33.6 实战排查案例

我记得有一次,一个第三方应用反馈说无法读取外部存储的图片。我检查了清单文件——READ_EXTERNAL_STORAGE声明了。运行时权限也申请了。但就是读不到。

我打开logcat,搜avc: denied,果然有一条:

avc: denied { read } for pid=5678 comm="thirdpartyapp" 
    name="DCIM" dev="mmcblk0p25" ino=12345 
    scontext=u:r:untrusted_app:s0:c512,c768 
    tcontext=u:object_r:userdata_file:s0 tclass=dir permissive=0

问题出在tcontextuserdata_file,而应用期望的是fuse_filemedia_rw_data_file。原来是存储挂载方式变了,SELinux策略没跟上。最终通过修改file_contexts配置解决了问题。

避坑指南:我曾经在调试一个系统应用时,发现所有权限都对了,但功能就是不行。最后发现是SELinux策略里allow规则写错了domain。记住:系统应用也有SELinux限制,别以为有系统签名就万事大吉。

33.7 总结

权限调试,说白了就是一层一层往下查。先看清单声明,再看运行时申请,然后确认权限组逻辑,最后查SELinux。大部分问题卡在SELinux这一层,因为它的日志不够直观,很多人不会看。

嗯,这里再强调一句:不要一上来就关SELinux。先看日志,找到拒绝的原因,再针对性解决。关SELinux只是验证手段,不是解决方案。


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