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 里的“长时间无响应”区间极其有用。
我的个人习惯是:新项目一律用 ExoPlayer,哪怕只是播个短音频。因为你不知道哪天产品就要求支持 HLS、DASH,或者要接入 DRM。MediaPlayer 那一套扩展起来真叫一个头疼。

1.3 ExoPlayer 的骨架——核心组件总览

在动手排查 ANR 之前,你得先记住 ExoPlayer 的“五脏六腑”。下面这张图是我自己整理的架构,每个节点都对应一个可能卡死的地方。

ExoPlayer 核心组件架构图 Player 接口 ExoPlayer 实例 MediaSource (媒体源 / 流解析) Renderer (解码 + 渲染) TrackSelector (轨道选择) LoadControl (加载策略 & 缓冲) ⭐ 排查 ANR 重点关注: MediaSource 的网络IO阻塞、Renderer 的解码线程死锁、LoadControl 持有锁导致主线程等待。

看到这个图,你大概能理解 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 的锁上
MediaSourceprepareSource 做网络 IO 超时抓取播放线程(ExoPlayer:MediaSource)堆栈
Renderer解码器内部死锁 / 渲染循环阻塞关注 RendererThread 的 lock contention
TrackSelector轨道切换时在主线程同步回调检查 onPlaybackParametersChanged 里的耗时操作
LoadControlonPrepared / onContinueLoading 阻塞查看 LoadControl 自定义实现是否调用了同步网络请求
⚠️ 避坑提醒:我曾经在自定义 LoadControl 的 getTargetBufferBytes() 方法里调了 Thread.sleep() 来模拟网络延迟测试——结果线上 ANR 直接炸了。记住:ExoPlayer 内部线程池的任何同步操作都可能被主线程间接等待,尤其是通过 Handler 串行化的时候。

1.6 组件初始化顺序与线程模型

ExoPlayer 初始化时,ExoPlayer.Builder 会创建默认组件。如果你没传自定义 RenderersFactory,它会根据系统解码器自动构建。注意:每个 Renderer 独立一个线程,而 MediaSource 的加载是在播放线程(名字类似 ExoPlayer:MediaSource)上进行的。所以当 ANR 发生时,你至少可以看三个方向的堆栈:

  1. 主线程:检查是否在等 ExoPlayer 的内部锁。
  2. 播放线程:是否在解析网络流或处理 manifest。
  3. Renderer 线程:是否卡在解码器 flush() 或 drain()。

我用这个模型解决过至少 5 个线上 ANR。其实思路很简单:知道每个组件跑在哪个线程,然后看堆栈里线程状态。但很可惜,很多人连 ExoPlayer 有几个线程都不清楚。

1.7 本章小结:记住这张图就够了

回到开始的架构图。你不需要背下所有源码,但一定要印在脑子里:Player 接口 → ExoPlayer 实例 → MediaSource / Renderer / TrackSelector / LoadControl。后续章节我会带你逐个从代码层面深挖它们如何制造 ANR,以及怎么用 StrictMode、Systrace、Firebase Crashlytics 快速定位。

一句话记核心:ExoPlayer 的 ANR 九成九是主线程和播放线程之间的锁冲突或是 IO 阻塞。而锁冲突往往就出在上面表格里那几个回调方法中。