ExoPlayer 库的加载与初始化
说到 ExoPlayer 的初始化,很多人上来就是 new ExoPlayer.Builder(context).build(),然后就开始怼播放逻辑。嗯,这么做项目是能跑,但线上卡顿、首帧慢、甚至内存暴涨,往往就埋在这第一步里。
我优化过好几个播放器启动的场景,说白了,初始化阶段的核心就两件事:库的加载 和 Player 实例的创建。这两步做不好,后面再好的解码策略也白搭。
库加载——被忽略的启动瓶颈
ExoPlayer 的库加载主要分两个层面:
- Gradle 依赖声明:你引入的模块越多,ProGuard 或 R8 在打包时合并的 class 就越多,APK 体积自然大,但这不是重点。
- 运行时首次加载:当应用冷启动、第一次调用
ExoPlayer.Builder时,ClassLoader 需要把相关的 Renderer、DataSource、MediaSource 等类全部加载到内存。
关键指标:我监控过一个项目,纯粹因为依赖了用不到的扩展(比如 RTMP、HLS 的某些解析器),导致 Player 创建耗时从 8ms 飙升到 120ms。后者发生在冷启首帧时,用户直接感觉「点播放后黑屏了 1 秒」。
为什么会有这么大的差距?因为 ExoPlayer 的类加载是按需懒加载的,但 Player.Builder 在 build() 时会触发一次内部的 DefaultRenderersFactory 初始化,里面会根据你注册的扩展,去加载对应 Renderer 的 Class。如果你不小心引入了非必要的依赖,这些类就会被加载进来,即使你根本不使用它们。
个人建议:严格使用 implementation 而不是 api,并且只引入你真正需要的 module。例如只播放 HLS 就只加 exoplayer-hls,别顺带把 exoplayer-rtmp 也拖进来。
初始化流程——逐阶段解析
我们来看标准的 ExoPlayer 初始化流程,我把它拆成 5 个阶段:
- Builder 创建:配置带宽、缓存、渲染器工厂等。
- build() 调用:实例化内部组件(TrackSelector、LoadControl、MediaSource.Factory 等)。
- setMediaSource():解析媒体来源,准备底层数据源。
- prepare():启动播放器内部状态机,开始缓冲区准备。
- setPlayWhenReady(true):真正开始渲染。
其中 build() 和 prepare() 是最容易踩坑的地方。我曾经在团队里看到有人把 setMediaSource 和 prepare 写在 UI 线程里,导致丢帧掉得飞起。
// 错误示范:主线程直接阻塞
ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSource source = new ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(url));
player.setMediaSource(source);
player.prepare(); // 可能触发IO或网络
player.setPlayWhenReady(true);
我曾经接手过一个项目,上述代码跑在 Activity.onCreate() 里,用户首页加载慢得像 PPT。定位后发现 prepare() 内部会同步创建 DefaultTrackSelector 的默认音视频选择逻辑,耗时约 30ms。解决方案是:把 prepare() 放到子线程或者使用异步 prepare(ExoPlayer 从 2.12 开始支持 prepare() 异步调用)。
优化手段:懒加载与预加载的平衡
初始化优化的核心思路其实就四个字:按需启动。别一股脑把所有东西都塞进 App 启动阶段。
| 优化策略 | 适用场景 | 延迟收益 |
|---|---|---|
| 延迟创建 Player | 用户点击播放才初始化 | 冷启阶段完全无负担 |
| 预创建 Player 但延迟 prepare | 列表页提前创建实例 | 减少点击后创建耗时 |
| 使用 PlayerView 的自动管理 | 只需布局占位 | 精简代码但牺牲控制力 |
我个人的习惯是:只在用户真正进入播放页时创建 Player。如果你用的是首页有视频、但用户可能滑动切换的场景,可以提前 500ms 在后台线程 build() 出一个 Player 实例,缓存起来。但注意:千万别提前调用 prepare(),因为不知道用户究竟要点哪个视频。
小技巧:如果需要预创建 Player,可以这样:
// 后台线程预构建
new Thread(() -> {
ExoPlayer player = new ExoPlayer.Builder(context)
.setRenderersFactory(new DefaultRenderersFactory(context))
.setLoadControl(new DefaultLoadControl())
.build();
// 存入弱引用池,待用户点击时取出使用
playerPool.put(player);
}).start();
注意 DefaultRenderersFactory 的构造函数允许传入 extensionRendererMode 参数,设置成 EXTENSION_RENDERER_MODE_OFF 可以跳过扩展加载,进一步提速。
初始化阶段的整体结构
下面这张图把整个初始化链路的节点和时序整理了一下,你可以直观看到哪些环节可以并行或延迟。
避坑指南
- 内存泄漏:我曾经在一个模块里把 ExoPlayer 实例作为 Activity 的成员变量,但没在 onDestroy 里 release,导致整个 Activity 无法被回收。一定要注意生命周期绑定。
- 重复初始化:如果用户在列表页快速滑动了十几次,每次滑出都创建和销毁 Player,那初始化开销会被放大。建议使用 ViewHolder 复用时复用 Player 实例,或者使用对象池。
- 线程阻塞:
new DefaultHttpDataSource.Factory()默认不设置 connectTimeout,某些弱网环境下 DNS 解析会卡住主线程。我习惯在 Factory 里显式设置超时:.setConnectTimeoutMs(5000).setReadTimeoutMs(8000)。
总结一句话:初始化阶段的性能调优,就是「能懒则懒,能异步则异步,能复用则复用」。你想想看,用户点播放按钮的那一瞬间,系统要加载几十个类、创建一堆对象,如果我们能让这些工作在后台悄悄完成,用户感知到的就是瞬间响应。