1. ExoPlayer 概述与核心组件
1.1 到底什么是 ExoPlayer?
说实话,我刚入行 Android 开发时,用的还是 MediaPlayer 那个老古董。视频播放一旦出问题,你根本不知道是底层解码器挂了,还是网络流断了。后来 Google 在 2014 年推出了 ExoPlayer,我才算真正看清了播放器内部的脉络。
ExoPlayer 是一个开源的、基于 App 级别的多媒体播放库。它不是系统组件,而是纯 Java/Kotlin 写的,所以你能拿到全套源码。我记得第一次看源码时惊叹:原来播放器的每一帧、每一个 buffer 的流动,都可以被我掌控。这就为排查 ANR 打下了底子——你想想,如果连源码都看不到,你排查什么?
1.2 为什么我推荐你用 ExoPlayer 做排查基础?
- 可控性强:MediaPlayer 把解码、渲染、缓冲全塞黑盒里,而 ExoPlayer 把每个环节拆成了独立的组件——这意味着如果你遇到 ANR,可以精准定位到是 MediaSource 卡住了,还是 Renderer 阻塞了。
- 支持自定义:我在项目中遇到过诡异的网络流卡死,当时就是通过重写
LoadControl和BandwidthMeter才追踪到是某 CDN 节点响应超时导致的 ANR。 - 调试能力:ExoPlayer 提供
AnalyticsListener,你可以监听每一个事件的时间戳——这对排查 ANR 里的“长时间无响应”区间极其有用。
1.3 ExoPlayer 的骨架——核心组件总览
在动手排查 ANR 之前,你得先记住 ExoPlayer 的“五脏六腑”。下面这张图是我自己整理的架构,每个节点都对应一个可能卡死的地方。
看到这个图,你大概能理解 ExoPlayer 的运行逻辑:你通过 Player 接口下发一个播放命令,ExoPlayer 实例 开始干活——它把媒体文件拆成多个 MediaSource(比如一个视频轨 + 一个音频轨),然后交给 TrackSelector 决定播放哪几个轨道,再丢给 Renderer 去解码渲染。而 LoadControl 在背后盯着 buffer 水位,决定什么时候从网络拉数据、什么时候暂停。
1.4 逐个分解核心组件
1.4.1 Player 接口 —— 你的遥控器
Player 定义了 play()、pause()、seekTo()、setPlaybackParameters() 等等抽象方法。说白了,它就是一套标准协议,不管底层用 ExoPlayer 还是别的实现,上层代码调用的接口是一样的。在 ANR 问题中,主线程如果调用了 Player.seekTo() 但被某个内部锁阻塞,就会卡住 UI。这是常见的坑。
1.4.2 ExoPlayer 类 —— 真正的调度中枢
ExoPlayer 把上面那些组件粘合起来。它内部维护了一个 PlaybackThread(播放线程),大部分耗时操作不在主线程执行。但有些回调(比如 Player.Listener)是在主线程触发的——我曾经遇到过一个 ANR 就是因为 onPlayerError 回调里做了数据库写入导致主线程等待。
1.4.3 MediaSource —— 流数据的源头
常见实现有 ProgressiveMediaSource(支持 MP4 等本地文件)、HlsMediaSource、DashMediaSource。它的核心是 prepareSource() 和 maybeThrowSourceInfoRefreshError()。如果网络不通或者 manifest 解析死循环,这里就会卡死。我排查过一个 ANR,最终发现是某个 HLS 流的 m3u8 文件格式异常导致解析线程陷入无限循环。
1.4.4 Renderer —— 解码渲染的工人
渲染器家族包括 MediaCodecVideoRenderer、MediaCodecAudioRenderer、TextRenderer(字幕)。每个 Renderer 运行在自己的线程上,从 MediaSource 取数据,解码,然后输出到 Surface 或 AudioTrack。ANR 经常发生在 render 循环里——比如解码器 flush 时主线程调了 setVideoSurface(),而 Renderer 内部正在等待解码器释放,结果形成死锁。
1.4.5 TrackSelector —— 选哪个轨道?
默认的 DefaultTrackSelector 会根据设备能力和网络状况自动选择最佳音视频轨道。但如果你手动设置了 TrackSelectionOverride,可能会触发重新评估,这时候如果评估逻辑里有同步操作(比如读数据库),就可能在主线程上造成 ANR 风险。我个人建议:轨道选择尽量在后台线程完成,别在 onPlayerStateChanged 里直接调。
1.4.6 LoadControl —— 缓冲的阀门
它决定什么时候开始加载数据、什么时候停止。默认实现 DefaultLoadControl 会控制 buffer 的最小/最大时长。ANR 的常见场景是:LoadControl.onPrepared() 里做了耗时的网络测速,导致主线程等待。另外,如果你自定义了 LoadControl,记得避开主线程锁。
1.5 组件与 ANR 的关系速查表
| 组件 | 常见 ANR 触发点 | 排查方向 |
|---|---|---|
| Player 接口 | seekTo / stop 在主线程等待内部锁 | 检查主线程堆栈,看是否卡在 ExoPlayerImpl 的锁上 |
| MediaSource | prepareSource 做网络 IO 超时 | 抓取播放线程(ExoPlayer:MediaSource)堆栈 |
| Renderer | 解码器内部死锁 / 渲染循环阻塞 | 关注 RendererThread 的 lock contention |
| TrackSelector | 轨道切换时在主线程同步回调 | 检查 onPlaybackParametersChanged 里的耗时操作 |
| LoadControl | onPrepared / onContinueLoading 阻塞 | 查看 LoadControl 自定义实现是否调用了同步网络请求 |
LoadControl 的 getTargetBufferBytes() 方法里调了 Thread.sleep() 来模拟网络延迟测试——结果线上 ANR 直接炸了。记住:ExoPlayer 内部线程池的任何同步操作都可能被主线程间接等待,尤其是通过 Handler 串行化的时候。
1.6 组件初始化顺序与线程模型
ExoPlayer 初始化时,ExoPlayer.Builder 会创建默认组件。如果你没传自定义 RenderersFactory,它会根据系统解码器自动构建。注意:每个 Renderer 独立一个线程,而 MediaSource 的加载是在播放线程(名字类似 ExoPlayer:MediaSource)上进行的。所以当 ANR 发生时,你至少可以看三个方向的堆栈:
- 主线程:检查是否在等 ExoPlayer 的内部锁。
- 播放线程:是否在解析网络流或处理 manifest。
- Renderer 线程:是否卡在解码器
flush()或drain()。
我用这个模型解决过至少 5 个线上 ANR。其实思路很简单:知道每个组件跑在哪个线程,然后看堆栈里线程状态。但很可惜,很多人连 ExoPlayer 有几个线程都不清楚。
1.7 本章小结:记住这张图就够了
回到开始的架构图。你不需要背下所有源码,但一定要印在脑子里:Player 接口 → ExoPlayer 实例 → MediaSource / Renderer / TrackSelector / LoadControl。后续章节我会带你逐个从代码层面深挖它们如何制造 ANR,以及怎么用 StrictMode、Systrace、Firebase Crashlytics 快速定位。