第2章 权限体系基础认知
Android权限等级分类、ExoPlayer依赖权限清单、运行时权限与清单声明差异

现象识别:权限不足时ExoPlayer播放失败的表现

当ExoPlayer缺少必要权限时,播放行为会出现以下可观察的异常现象。请对照下表快速判断是否属于权限问题:

现象典型日志关键字疑似权限类型
播放器状态直接变为 STATE_ENDED,无任何缓冲MEDIA_ERROR_IO 或 Permission denied文件/网络访问权限
视频黑屏但音频正常DRM license request failed网络权限(若license需要联网)
播放本地文件时抛 FileNotFoundExceptionopen failed: EACCES存储读取权限
使用自定义DataSource时卡在连接阶段Unable to resolve host网络权限
经验规则:如果logcat中出现 Permission denied、EACCES(针对文件)或 Network error(且URL有效),优先检查权限配置。

数据采集:权限相关信息的获取方法

排查权限不足前提是准确采集以下三类数据。每类数据的获取方式与关注点如下:

  1. 应用私有权限状态 —— 通过 ContextCompat.checkSelfPermission() 或 PackageManager.checkPermission() 获取当前授予状态。
  2. AndroidManifest.xml 声明 —— 检查 <uses-permission> 标签是否包含了所有 ExoPlayer 工作所需的权限。
  3. 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+ 对直接文件路径限制)。
ExoPlayer权限不足故障树分析 ExoPlayer 播放失败 (STATE_ENDED / IO异常) 权限等级分类错误 依赖权限清单遗漏 声明与运行时差异 普通权限当作危险权限(不必要运行时请求) 危险权限当作普通权限(仅声明未请求) 本地播放缺少 READ_EXTERNAL_STORAGE 网络播放漏声明 INTERNET DRM License 网络权限遗漏 有声明但没调用运行时请求 运行时请求回调未处理“拒绝” 故障树使用说明 从顶部根节点开始,沿分支查找播放失败的具体权限原因。 每个叶子节点对应一种常见错误场景,可直接指导修复动作。

修复动作:按故障树节点执行对应操作

根据故障树定位到具体节点后,执行下表对应的修复步骤:

故障树节点修复动作代码/配置示例
普通权限误做运行时请求移除不需要的运行时请求代码,仅保留清单声明即可删除 requestPermissions 中 INTERNET 的调用
危险权限未运行时请求实现 ActivityCompat.requestPermissions() 并在回调中处理授予/拒绝
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE);
本地播放遗漏存储权限在清单中添加 <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,弹出引导用户去设置
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    // 重新初始化ExoPlayer
} else {
    // 提示用户手动开启权限
}

复测:验证权限修复后的播放能力

执行以下复测清单,确保权限问题完全解决:

  • 单元检测:在播放前添加 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权限基线

在项目早期将这些规则固化为团队规范,防止权限问题反复出现:

  1. 权限清单模板:维护一个标准 AndroidManifest.xml 片段(见下),每次新建ExoPlayer功能时直接复制。
  2. 运行时请求封装:制作统一的 ExoPlayerPermissionHelper 类,集中处理危险权限请求及回调。
  3. 代码审查检查项:每次MR必须确认:
    - 新引入的 MediaSource 是否需要额外权限?
    - 是否遗漏了 AndroidManifest.xml 中的声明?
    - 危险权限的运行时请求和拒绝处理是否完整?
  4. 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权限表再写代码。