1. ExoPlayer 简介与核心架构
做 Android 多媒体开发这行,如果你还没碰过 ExoPlayer,那我建议你赶紧上手。我在几年前接手一个视频播放项目时,就踩过 MediaPlayer 的不少坑——黑屏、内存泄漏、格式支持不全……后来切到 ExoPlayer,才感觉真正「松了一口气」。
ExoPlayer 是 Google 开源的一个播放器框架。它不像 MediaPlayer 那样是一个系统组件,而是一个纯 Java/Kotlin 实现的库。说白了,你可以把它当成一个高度可定制的「播放引擎」,想怎么改就怎么改。
那它凭什么比系统播放器更香?我总结了几点:
- 分层架构——内核和渲染层完全解耦,你可以单独替换某个模块
- 丰富的协议支持——DASH、HLS、SmoothStreaming、RTSP、本地文件……我见过的格式基本全覆盖
- 可扩展性强——自定义渲染器、自定义数据源、自定义带宽估算器……连 DRM 方案都能自己插
- 主动维护——Google 一直在更新,性能优化和新功能(比如无缝切换、音频焦点处理)都在跟进
不过我刚开始用 ExoPlayer 时也有点懵——它不像 MediaPlayer 那样 new 一个对象就完事。你需要初始化一个 ExoPlayer.Builder,配置各种组件,再准备媒体源。这个过程如果没处理好,很容易造成启动慢、首帧延迟高。我们这门课就是专门讲如何优化初始化阶段的性能,所以先得把架构搞明白。
1.1 核心架构概览
先看一张整体架构图,画的是 ExoPlayer 内部的主要模块和它们的关系。你在调优初始化时,脑子里要有这张图。
架构图中最核心的就是中间那层——组件容器。你初始化 ExoPlayer 时,实际上就是在装配这个容器。我习惯把 ExoPlayer 的初始化分为三个步骤:
- 创建 ExoPlayer 实例——使用
ExoPlayer.Builder(context),内部会帮你实例化所有默认组件。 - 配置依赖组件——比如替换自定义的
RenderersFactory、DataSource.Factory、LoadControl等。 - 设置媒体源——通过
player.setMediaItem()传入媒体地址,准备好后才调用player.prepare()。
这里有一个很容易踩的坑:所有的组件配置最好在 prepare() 之前完成。如果你在 prepare() 之后才修改某些参数,可能会导致组件重建,白白浪费性能。我曾经有过一次线上事故,就是因为动态修改了缓存策略,结果每次切视频都会触发全量重新初始化,首帧延迟直接飙到 3 秒多……
1.2 主要模块职责与初始化时序
下面这个表格整理了初始化阶段涉及的几个关键模块,以及它们各自的「开销点」。你在做性能调优时,脑子里必须清楚每个模块大概要花多少时间。
| 模块 | 职责 | 初始化开销 | 常见优化方向 |
|---|---|---|---|
| ExoPlayerImpl | 管理播放状态、事件回调 | 低(对象创建) | 懒加载?不推荐,会影响首帧 |
| RenderersFactory | 创建音视频渲染器 | 中(反射+对象创建) | 预创建渲染器实例、减少不必要的解码器 |
| DataSource.Factory | 提供网络/文件数据源 | 低(工厂模式) | 复用连接池、预建立 Http 连接 |
| LoadControl | 控制缓冲策略(时长、水位) | 低(参数设置) | 根据场景调低初始缓冲阈值 |
| TrackSelector | 选择最佳音视频轨道 | 中(解析媒体信息) | 减少轨道解析数量、预先设定默认轨道 |
| MediaSource | 封装媒体数据来源(如 DashMediaSource) | 高(下载 manifest 并解析) | 预加载 manifest、缓存解析结果 |
核心调优原则:初始化阶段的目标是「在用户看不到的地方提前做好所有准备工作」。也就是说,不要在播放按钮按下后才去创建 ExoPlayer、解析 manifest、建立网络连接。你应该在 onCreate() 或界面进入前就完成这些操作,然后通过 player.prepare() 快速触发播放。
1.3 初始化流程的典型时序
为了让你直观感受整个初始化链条,我画个典型的时间线:
0ms:用户点击播放按钮
+-- 2ms:创建 ExoPlayer.Builder(context) —— 几乎无开销
+-- 5ms:配置 RenderersFactory / DataSource.Factory —— 主要耗在自定义组件实例化
+-- 10ms:创建 MediaSource —— 如果是网络媒体,需要下载 manifest(50~200ms 不等)
+-- 60ms:调用 player.setMediaItem() —— 内部开始准备媒体源
+-- 90ms:调用 player.prepare() —— 触发 renderer 创建、TrackSelector 解析
+-- 120ms:首帧渲染 —— 成功显示第一帧画面
你看,从点击到首帧,乐观情况下也要 120ms 左右。如果网络慢、manifest 大、渲染器创建时还走了反射,轻松突破 500ms。我们后面的课程会针对每个环节给出具体优化方案。
1.4 一个容易被忽略的初始化陷阱
曾经有个项目,我在做视频列表的快速滑动播放时,每次进入新视频都重新创建了 ExoPlayer。结果滑动一快,内存暴增、GC 频繁,首帧时间直接飙升到1秒以上。最后我用 PlayerView + 复用 ExoPlayer 实例 + 只切换 MediaItem 的方式,把首帧稳定在100ms以内。
所以记住:不要频繁创建和销毁 ExoPlayer 实例。同一个播放器实例可以反复 setMediaItem() + prepare(),内部组件会被复用,性能好得多。
1.5 小结:初始化阶段你该做什么?
我个人习惯把初始化阶段分成两个子阶段:
- 预初始化(用户还没点击播放)——创建 ExoPlayer 实例、配置好所有组件(数据源工厂、加载控制、轨道选择器)。
- 快速启动(用户点击播放)——直接调用
setMediaItem()+prepare(),尽可能减少耗时。
你想想看,如果预初始化做得好,用户点击后基本就是等待 manifest 下载和解析,也就几十到几百毫秒。而如果每次点击都从头创建 ExoPlayer……那初期化阶段就要吃掉好几倍的时间。
一个小技巧:如果你的 App 有多个页面共用 ExoPlayer,可以考虑把 ExoPlayer 实例做成 Application 级别的单例。不过要注意生命周期管理——播放完毕或界面不可见时要 stop() 并释放资源,不然会一直占用解码器和网络连接。我自己一般在 BaseActivity 中用 onStart() / onStop() 来绑定和释放播放器。