ExoPlayer 初始化流程概述
做性能优化,第一个要摸透的就是初始化流程。说白了,播放器从创建到真正能播,中间到底干了哪些事?哪些地方可能会卡住主线程?哪块能省时间?
我刚开始做视频播放的时候,以为初始化就是 new 一个 ExoPlayer,调个 prepare 就完了。结果上线后用户反馈「点开视频要等两三秒,黑屏转菊花」……嗯,后来我才明白,初始化阶段埋着不少坑。
ExoPlayer 的初始化跟系统 MediaPlayer 完全不同——它不是黑盒,而是由你组装各个模块。你给什么 Renderers、什么 TrackSelector、什么 LoadControl,它就怎么跑。所以性能好不好,很大程度上取决于你在这个阶段的配置。
第一步:Builder 装配阶段
没错,一切从 ExoPlayer.Builder(context) 开始。这个阶段其实就是「搭积木」。你往 Builder 里塞 RenderersFactory、TrackSelector、LoadControl、AnalyticsCollector 等等。这个阶段本身几乎不耗时——它只是在堆配置对象。
常见误区:把耗时操作放到 Builder 的回调里。
比如有人会在 RenderersFactory 里 new MediaCodecVideoRenderer 的时候去做网络请求——这就把主线程堵住了。Builder 的构造和 build() 都是主线程调用的,千万别在这里搞 I/O。
我的习惯:把 Builder 的所有配置参数提前准备好,尽量复用对象。
比如 DefaultTrackSelector、DefaultLoadControl 这些,可以在 Application 的 onCreate 里 new 好,然后传给多个 Player 实例。这样能省一点 GC 和对象创建的时间。
第二步:build() 创建 Player 实例
调用了 builder.build() 之后,ExoPlayer 内部会做几件事:
- 创建内部 HandlerThread(播放器线程)
- 实例化 ExoPlayerImpl(真正的播放器对象)
- 初始化各个组件,比如创建默认的 MediaSource.Factory
- 注册系统广播接收器等
这里有一个容易被忽略的点——MediaSource.Factory 的创建时机。我记得在某个版本里,如果你没有显式设置 setMediaSourceFactory,build() 内部会用反射去加载 DefaultMediaSourceFactory。反射虽然快,但在低端机上还是能感觉到卡顿的。所以我建议你手动传一个 MediaSource.Factory 进去,省掉反射开销。
一个坑:我曾经在列表页快速滑动时,每次创建新 Player 都走 build(),结果低端机上频繁 GC 导致掉帧。后来改成 Player 池化,或者复用 MediaSource.Factory 和 RenderersFactory,帧率就稳了。
第三步:setMediaSource() 真的只是“设个来源”吗?
很多人以为 player.setMediaSource(mediaSource) 就是存一下引用。其实不是的。
它会触发 MediaSource 的 preparation 逻辑,不过注意——prepare() 还没调用的时候,MediaSource 只会做轻量初始化。比如 HlsMediaSource 会检查 manifest 的 URI 是否合法;DashMediaSource 也会做一些格式校验。这些操作虽然不耗时,但如果你的 MediaSource 是自己实现的,而且构造时就跑去解析很大文件……那就会坑人。
避坑指南:setMediaSource 只做同步轻量校验,不要把网络请求、文件解析扔到这一步。
如果你想提前加载数据,请用 player.prepare() 触发异步加载,或者用 MergingMediaSource 按需拼接。
第四步:prepare() —— 真正的“开始时差”
调用 player.prepare() 之后,播放器线程开始工作。这一步才是初始化耗时的重头戏。
| 子阶段 | 行为 | 可能耗时 |
|---|---|---|
| MediaSource 准备 | 加载 manifest / playlist,解析格式 | 网络+解析:几十ms到数秒 |
| 渲染器初始化 | 创建 decoder,配置 surface,建立音轨 | 解码器创建:10~200ms |
| 缓冲区建立 | 根据 LoadControl 配置分配 buffer | 几乎无感 |
| 首帧解码/预取 | 读入第一段数据并尝试解码 | 取决于码率 & 解码器 |
这里面,MediaSource 准备和解码器初始化是两个最大头。如果用户网络慢,manifest 下载就要等;如果设备解码器硬解不稳定,系统 fallback 到软解,那就更慢了。
第五步:播放器状态流转
prepare() 结束后,播放器状态变为 STATE_READY(如果没有错误)。这时候你才能调 player.play() 真正开始播放。
不过别忘了,从 STATE_READY 到真正出声/出画面,中间还有一个首帧输出的时间。这属于播放阶段,但很多人在统计「首屏时间」时会把初始化+这一步都算进去。我建议你分开打点,方便定位到底是初始化慢还是解码慢。
个人实践:在 Player.Listener 的 onPlaybackStateChanged 和 onRenderedFirstFrame 里打上 Log。
用 SystemClock.elapsedRealtime() 记录每个阶段的时间戳。这样一分析就能看出瓶颈在哪。
总结一张脑图
上面那张 SVG 已经把初始化流程串联起来了。你盯着它看,会发现:
- 上半部分(Builder → build())是主线程同步操作,别搞耗时逻辑。
- 下半部分(setMediaSource → prepare)是异步后台线程,但 prepare 内部可能阻塞等待网络/解码器。
- 红色节点(解码器初始化)是最容易被忽视的性能杀手。