第2章 权限体系基础认知
Android权限等级分类、ExoPlayer依赖权限清单、运行时权限与清单声明差异
现象识别:权限不足时ExoPlayer播放失败的表现
当ExoPlayer缺少必要权限时,播放行为会出现以下可观察的异常现象。请对照下表快速判断是否属于权限问题:
| 现象 | 典型日志关键字 | 疑似权限类型 |
|---|---|---|
播放器状态直接变为 STATE_ENDED,无任何缓冲 | MEDIA_ERROR_IO 或 Permission denied | 文件/网络访问权限 |
| 视频黑屏但音频正常 | DRM license request failed | 网络权限(若license需要联网) |
播放本地文件时抛 FileNotFoundException | open failed: EACCES | 存储读取权限 |
| 使用自定义DataSource时卡在连接阶段 | Unable to resolve host | 网络权限 |
经验规则:如果logcat中出现
Permission denied、EACCES(针对文件)或 Network error(且URL有效),优先检查权限配置。
数据采集:权限相关信息的获取方法
排查权限不足前提是准确采集以下三类数据。每类数据的获取方式与关注点如下:
- 应用私有权限状态 —— 通过
ContextCompat.checkSelfPermission()或PackageManager.checkPermission()获取当前授予状态。 - AndroidManifest.xml 声明 —— 检查
<uses-permission>标签是否包含了所有 ExoPlayer 工作所需的权限。 - ExoPlayer 内部错误回调 —— 在
Player.Listener.onPlayerError中获取ExoPlaybackException的 cause,提取权限相关异常。
// 典型数据采集代码片段(仅示例,非完整)
int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE);
if (permissionCheck == PackageManager.PERMISSION_DENIED) {
Log.w("ExoPlayer", "存储权限未授予,可能影响本地播放");
}
同时导出 adb shell dumpsys package com.your.package 可查看已声明的权限清单。
原因定位:Android权限等级与ExoPlayer依赖清单
Android 权限按照保护等级分为三类,ExoPlayer 常用的权限恰好覆盖这三类。理解等级差异是定位问题的核心:
| 权限等级 | 特点 | ExoPlayer 常见依赖 | 是否需要运行时请求 |
|---|---|---|---|
| 普通权限(normal) | 仅在清单声明即自动授予 | INTERNET(网络播放)、ACCESS_NETWORK_STATE | 否 |
| 危险权限(dangerous) | 需要运行时动态请求,用户可拒绝 | READ_EXTERNAL_STORAGE(本地文件播放)、RECORD_AUDIO(录制后播放) | 是 |
| 特殊权限(special) | 需系统设置手动开启,权限请求流程不同 | SYSTEM_ALERT_WINDOW、WRITE_SETTINGS(非ExoPlayer核心,仅特定场景) | 特殊处理 |
常见误区:
INTERNET 虽然是普通权限,但如果在AndroidManifest.xml中遗漏声明,ExoPlayer 对网络URL的访问将静默失败(IOException)。清单声明是所有权限生效的前提,即使普通权限也不能省略。
ExoPlayer 依赖权限详细清单(完整排查表)
根据 ExoPlayer 官方文档及常见使用场景,按功能模块整理依赖权限:
| 功能模块 | 所需权限 | 等级 | 备注 |
|---|---|---|---|
| 网络流播放(HLS、DASH、SS) | INTERNET、ACCESS_NETWORK_STATE | 普通 | 无运行时请求 |
| 本地文件播放(mp4、mkv 等) | READ_EXTERNAL_STORAGE(Android 12 及以下)或 READ_MEDIA_VIDEO/READ_MEDIA_AUDIO(Android 13+) | 危险 | 必须运行时请求 |
| DRM 保护内容播放 | INTERNET(获取License) | 普通 | 注意License服务器可能还需要额外的网络权限 |
| 自定义 MediaSource 或 DataSource | 取决于具体实现(如文件、ContentProvider、URL) | 动态 | 需开发者自查 |
| 后台播放或通知控制 | FOREGROUND_SERVICE、POST_NOTIFICATIONS(Android 13+) | 普通/危险 | 按Android版本和场景 |
运行时权限与清单声明的差异(定位关键)
很多权限问题源于开发者混淆了“声明”与“请求”两个动作。差异比对:
| 对比维度 | 清单声明(<uses-permission>) | 运行时请求(requestPermissions()) |
|---|---|---|
| 触发时机 | 编译期写入apk | 运行时动态弹窗 |
| 对危险权限的效果 | 仅告知系统app需要该权限,不自动授予 | 弹出系统对话框,用户选择授予/拒绝 |
| 对普通权限的效果 | 系统自动授予,无需请求 | 调用无意义(但不会出错) |
| 缺失后果 | 系统直接拒绝权限,checkSelfPermission 永远返回 DENIED | 无运行时弹窗,用户无法主动授予 |
| 常见错误 | 遗漏声明,但运行时请求了——仍被拒绝 | 已声明但未请求——危险权限无法获得 |
定位规则:
- 若 普通权限 缺失 → 检查清单声明。
- 若 危险权限 缺失 → 先检查清单声明,再检查运行时请求逻辑是否完整(含权限回调处理)。
- 若播放本地文件失败且存储权限已授予 → 检查是否使用了
- 若 普通权限 缺失 → 检查清单声明。
- 若 危险权限 缺失 → 先检查清单声明,再检查运行时请求逻辑是否完整(含权限回调处理)。
- 若播放本地文件失败且存储权限已授予 → 检查是否使用了
FileProvider(Android 11+ 对直接文件路径限制)。
修复动作:按故障树节点执行对应操作
根据故障树定位到具体节点后,执行下表对应的修复步骤:
| 故障树节点 | 修复动作 | 代码/配置示例 |
|---|---|---|
| 普通权限误做运行时请求 | 移除不需要的运行时请求代码,仅保留清单声明即可 | 删除 requestPermissions 中 INTERNET 的调用 |
| 危险权限未运行时请求 | 实现 ActivityCompat.requestPermissions() 并在回调中处理授予/拒绝 | |
| 本地播放遗漏存储权限 | 在清单中添加 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 并运行时请求 | 在 AndroidManifest.xml 的 <manifest> 内添加即可 |
| 网络播放遗漏 INTERNET | 在清单中添加 <uses-permission android:name="android.permission.INTERNET" /> | 同上,注意区分 Android 12 以上不需要 usesCleartextTraffic 但需声明权限 |
| DRM 网络权限 | 确保 INTERNET 权限声明,检查 License URL 是否可用 | 使用 adb shell dumpsys package permissions 验证 |
| 运行时请求回调未处理拒绝 | 在 onRequestPermissionsResult 中判断 grantResults,弹出引导用户去设置 | |
复测:验证权限修复后的播放能力
执行以下复测清单,确保权限问题完全解决:
- 单元检测:在播放前添加
checkSelfPermission日志,确认权限状态为GRANTED。 - 功能验证:播放受影响的媒体资源(本地文件/网络流),观察是否能正常进入
STATE_READY并开始渲染。 - 边界测试:拒绝权限后重新打开应用,确认应用不会崩溃且能给出友好提示。
- 自动化回归:使用
AndroidTest注入拒绝的权限状态,测试播放降级逻辑。
// 复测时可在Player.Listener中捕获错误
player.addListener(new Player.Listener() {
@Override
public void onPlayerError(ExoPlaybackException error) {
Throwable cause = error.getCause();
if (cause instanceof PermissionException ||
(cause.getMessage() != null && cause.getMessage().contains("Permission denied"))) {
// 权限问题未完全修复
}
}
});
预防:建立ExoPlayer权限基线
在项目早期将这些规则固化为团队规范,防止权限问题反复出现:
- 权限清单模板:维护一个标准
AndroidManifest.xml片段(见下),每次新建ExoPlayer功能时直接复制。 - 运行时请求封装:制作统一的
ExoPlayerPermissionHelper类,集中处理危险权限请求及回调。 - 代码审查检查项:每次MR必须确认:
- 新引入的 MediaSource 是否需要额外权限?
- 是否遗漏了AndroidManifest.xml中的声明?
- 危险权限的运行时请求和拒绝处理是否完整? - CI 检查:使用
lint或checkPermission.kt脚本扫描清单中的权限声明与运行时请求的一致性。
<!-- ExoPlayer 权限清单模板(完整版) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 本地文件播放(请根据minSdk选择正确权限名) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- 如需后台播放或通知 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
核心预防原则:
- 普通权限 → 只在清单声明“一次性”完成。
- 危险权限 → 清单声明 + 运行时请求,缺一不可。
- 每次引入新特性,先查ExoPlayer权限表再写代码。
- 普通权限 → 只在清单声明“一次性”完成。
- 危险权限 → 清单声明 + 运行时请求,缺一不可。
- 每次引入新特性,先查ExoPlayer权限表再写代码。