播放准备阶段的 ANR 分析
说到播放器 ANR,我个人的经验里,准备阶段 是最容易翻车的地方。为什么呢?因为 ExoPlayer 在准备阶段干的活特别杂——要拉 manifest、要建 DataSource、要初始化解码器,随便哪个环节卡住五秒以上,系统就给你弹个“无响应”对话框。
我记得有一次线上反馈,用户点开视频就卡死,复现率接近 40%。最后查到是主线程里直接调了 prepare(),而那个视频源是一个响应很慢的 HLS 流。你看,这坑早在准备阶段就埋下了。
1. 准备阶段的完整流程
ExoPlayer 从 player.prepare() 到回调 Listener.onPrepared(),中间会依次执行这些事情:
- 创建
MediaSource(例如 HlsMediaSource、DashMediaSource) - 内部创建
ChunkSource并初始化DataSource.Factory - 开始加载 Manifest(HLS 的 .m3u8、DASH 的 .mpd)
- 解析 Manifest,构建轨道数组
- (如果是本地文件)直接读取元数据
- 初始化解码器(或等待解码器预热)
- 回调
onPrepared
这里面任何一步放在主线程里做,一旦网络超时或磁盘阻塞,ANR 就来了。
main 线程堆栈里有没有 ExoPlayerImpl 或 MediaSource 相关的帧。如果有,十有八九是准备阶段被卡住。
2. 常见的 ANR 场景
| 场景 | 根因 | 典型堆栈片段 |
|---|---|---|
| 网络 Manifest 加载卡死 | 主线程调用 prepare(),内部同步网络请求超时 |
|
| 解码器初始化慢 | 同步创建 MediaCodec,厂商定制层耗时过长 | |
| MediaSource 构建过重 | 在主线程中创建带大量预加载的 MediaSource | |
我有个亲身经历:某直播 App 在 Android 9 上频繁 ANR,堆栈都落在 MediaCodec.configure() 上。排查后发现是某款芯片的解码器初始化锁冲突,而 ExoPlayer 默认在主线程同步等待解码器就绪。后来我们在 prepare() 之前提前 warm up 解码器实例,才把 ANR 压下去。
3. 排查工具与方法
准备阶段的 ANR 排查,我一般按这个顺序来:
- 抓取 ANR trace 文件:从
/data/anr/traces.txt或 BugReport 里找到主线程堆栈。 - 对比时间线:利用
Looper.getMainLooper().getThread().getStackTrace()加上 log 输出,或者直接上 Systrace。 - 看 ExoPlayer 内部日志:开启
ExoPlayer.setLoggingLevel(Log.INFO),留意Prepare和onPrepared的时间戳。 - 检查 DataSource:确认是否使用了
DefaultHttpDataSource且没有设置 connect timeout 或 read timeout。
prepare() 放到子线程里,以为能规避 ANR。但 ExoPlayer 内部仍然会在子线程回调某些同步操作(比如解码器配置),如果那个子线程是 HandlerThread 且消息队列被阻塞,回调照样卡住。我建议统一使用 prepare(Looper) 并确保子线程 Looper 不处理耗时任务。
4. 核心流程图:准备阶段 ANR 关键路径
下图展示了播放准备阶段容易产生 ANR 的节点。灰色框是主线程必经历程,红色框是高危卡点。
5. 快速定位代码示例
当你在 ANR trace 里看到类似下面的调用栈时:
"main" prio=5 tid=1 Blocked
at com.google.android.exoplayer2.source.hls.HlsMediaSource.prepareSourceInternal(HlsMediaSource.java:123)
at com.google.android.exoplayer2.ExoPlayerImpl.prepare(ExoPlayerImpl.java:456)
...
这就说明 prepareSourceInternal 里做了同步网络请求。我通常会在 DataSource.Factory 的创建处加两个关键参数:
DefaultHttpDataSource.Factory factory = new DefaultHttpDataSource.Factory()
.setConnectTimeoutMs(3000) // 3 秒连接超时
.setReadTimeoutMs(3000); // 3 秒读取超时
同时确保 player.prepare() 是在后台线程执行:
// 正确的做法
HandlerThread playThread = new HandlerThread("exo:play");
playThread.start();
Player player = new ExoPlayer.Builder(context)
.setLooper(playThread.getLooper())
.build();
player.prepare(mediaSource);
注意:即使 prepare 在后台线程,如果 Looper 的消息队列里后续有耗时操作,依然可能延迟回调。所以我习惯单独开一个专用于播放的 HandlerThread,不和 UI 线程混用。
onPrepared 回调中打印 currentPosition 和 SystemClock.elapsedRealtime(),对比 prepare() 调用时间,就能算出精确的准备耗时。如果超过 3 秒,就要怀疑网络或解码器问题了。
6. 总结排查 Checklist
- ✅ 确认
prepare()是否在主线程执行(建议统一使用setLooper到后台线程) - ✅ DataSource 超时设置是否合理(建议 connect + read 合计 ≤ 6 秒)
- ✅ MediaSource 是否在 UI 线程创建(高耗时操作如创建 HlsMediaSource 应后移至后台线程)
- ✅ 解码器初始化是否同步阻塞(可尝试提前 warm up 解码器或使用
MediaCodecList预查询) - ✅ 抓取完整 ANR trace,对比主线程堆栈是否存在 ExoPlayer 内部类的锁等待
prepare() 丢到异步线程后就撒手不管。记住,ExoPlayer 的内部状态机对线程安全要求较高,随意切换 Looper 可能导致状态不同步。建议始终使用同一个 Looper 执行所有播放操作。
准备阶段的 ANR 其实是最容易复现和修复的——只要你把 IO 操作从主线程剥离,并严格控制超时。我当年踩过的坑,基本都离不开“主线程等网络”或“主线程等解码器”这两个老朋友。希望今天的内容能帮你少走弯路。