播放器实例的创建与复用优化
播放器实例的创建,说白了就是 new 一个 ExoPlayer 对象出来。很多人觉得这步没什么,不就是 new 一下吗?但我告诉你,这个操作背后有不少隐藏成本。我自己在优化一个短视频应用时,发现仅仅因为频繁创建和销毁播放器,启动延迟就多出来 300ms 以上。
为什么创建播放器这么“贵”?
ExoPlayer 的创建不是简单的 new 对象。它会做以下几件事:
- 初始化内部的渲染器、解码器、音轨选择器
- 创建 HandlerThread 和 Looper 用于内部消息循环
- 加载默认的 LoadControl、TrackSelector 等组件
- 绑定到 Surface/TextureView(如果有)
- 预分配缓冲区(取决于 LoadControl 配置)
嗯,这些操作加起来,在低端机上可能超过 100ms。你想想看,如果每次进入页面都重新创建一次,用户得等多久?
复用策略:单例 vs 对象池
复用播放器实例是启动优化的老药方。我个人习惯分两种情况来用:
1. 页面级复用(推荐)
如果应用里只有少数几个页面需要播放视频(比如 Feed 流 + 详情页),直接在 Application 里持有一个全局播放器实例,谁用谁拿。缺点是不能同时播放多个视频,但 90% 的场景够用了。
// 简单单例实现
public class PlayerManager {
private static ExoPlayer player;
public static ExoPlayer getInstance(Context context) {
if (player == null) {
player = new ExoPlayer.Builder(context).build();
}
return player;
}
}
player.stop() 并重置 MediaItem,否则会残留上一曲的状态。我曾经遇到过因为没清理,导致切换时闪一下黑屏的问题。
2. 对象池复用(多实例场景)
如果应用需要同时播放多个视频(比如画中画、多视角直播),单例就不够用了。这时可以用一个固定大小的对象池,比如最多 3 个播放器实例。
public class PlayerPool {
private final ArrayDeque<ExoPlayer> pool = new ArrayDeque<>(3);
public synchronized ExoPlayer acquire(Context context) {
ExoPlayer player = pool.poll();
if (player == null) {
player = new ExoPlayer.Builder(context).build();
}
return player;
}
public synchronized void release(ExoPlayer player) {
player.stop();
player.clearMediaItems();
pool.offer(player);
}
}
这样既控制了内存占用,又避免了每次都 new。注意释放时要重置内部状态,防止泄漏。
配置优化:别在 Builder 里堆太多东西
很多人在创建播放器时往 Builder 里加一堆自定义组件:
new ExoPlayer.Builder(context)
.setRenderersFactory(...)
.setMediaSourceFactory(...)
.setLoadControl(...)
.setTrackSelector(...)
.build();
这会让创建时间成倍增加。我的建议是:只在 Builder 里设置你真正需要覆盖的组件,其余的用默认实现。默认的 DefaultRenderersFactory 和 DefaultLoadControl 已经经过了大量优化,没必要自己重写。
| 配置项 | 是否必须改? | 影响 |
|---|---|---|
| RenderersFactory | 通常不改 | 改可能导致解码器选择逻辑改变,增加初始化耗时 |
| LoadControl | 按需改 | 比如设置缓冲区大小,但默认的 DefaultLoadControl 已经够用 |
| TrackSelector | 按需改 | 比如强制使用某种音轨,改后影响很小 |
| MediaSourceFactory | 很少改 | 除非你要自定义 DASH/HLS 解析逻辑 |
避坑指南:复用时的状态清理
复用播放器最大的坑就是状态残留。还记得我前面说的那个黑屏问题吗?具体做法:
- 复用前调用
player.stop() - 调用
player.clearMediaItems()清空播放列表 - 重置
player.seekToDefaultPosition() - 如果之前设置了 Volume,记得恢复默认值
另外,如果你使用了 Player.Listener,复用时一定要解绑旧的监听器,否则会收到上个页面的回调,造成奇怪的 bug。我曾经因为没解绑,导致播放器切回来时自动暂停,用户反馈“点播放没反应”。排查了半天才发现是旧监听器里调了 player.pause()。
一张图理解创建与复用的本质
下面这张图展示了播放器实例从创建到复用的完整逻辑。核心就是:能复用就别创建,能轻量就别堆配置。
总结一下
播放器实例的创建与复用优化,核心就两件事:
- 能用复用就别新建 —— 单例或对象池,根据并发需求选
- 能轻量就别堆配置 —— 保留默认组件,只覆盖必要项
这两个动作加起来,能让你的启动速度提升 100~200ms。在短视频、直播这类对首帧时间敏感的场景下,这 100ms 可能就是用户体验的天壤之别。嗯,今天就先聊到这儿,你可以在自己的项目里试试看。