1. 概述:ExoPlayer启动流程与瓶颈分析

搞多媒体播放的兄弟都知道,用户打开视频那几秒的等待感有多要命。我接手过一个直播项目,启动花了快3秒,用户直接骂娘。说白了,ExoPlayer的启动速度直接影响留存率,今天咱们就把这层窗户纸捅破。

我先画个流程图,你大概看一眼整体脉络。后面咱们一截一截拆。

ExoPlayer 启动流程与瓶颈 创建 ExoPlayer 实例 new ExoPlayer.Builder(context).build() 配置 MediaSource ProgressiveMediaSource / HlsMediaSource... player.prepare() 异步加载 Manifest / 初始化 TrackSelector 解码器初始化 MediaCodec / AudioTrack / VideoRenderer 第一帧渲染 onRenderedFirstFrame() 回调 ⚡ 主要瓶颈 • 解码器创建 / 预热(200~800ms) • Manifest 远程下载(网络延迟) • 音频/视频 Track 初始化 • 第一帧渲染等待时间

启动流程概览

ExoPlayer 从创建到出第一帧,大致走五步:

  1. 创建实例 —— new Builder 一把梭,其实里面做了好多活,比如初始化 Looper、建立 HandlerThread。
  2. 配置 MediaSource —— 根据你的 URL 或文件生成对应的 MediaSource,本地文件还好,网络流就要开始 DNS 解析了。
  3. 调用 prepare() —— 这是异步的,但主线程还是得等消息回调。内部会下载 Manifest、创建 TrackSelector、初始化 Renderer。
  4. 解码器初始化 —— 这一步最要命,MediaCodec 创建、配置、Start,AudioTrack 也要折腾一遍。我遇到过有些低端机光解码器创建就花 500ms。
  5. 第一帧渲染 —— 视频渲染器收到第一帧数据后输出到 Surface,同时触发 onRenderedFirstFrame()。这个回调才是用户真正看到画面的时刻。

关键指标: 从 new ExoPlayer.Builder() 到 onRenderedFirstFrame() 的耗时,业界普遍追求 800ms 以内。超过 1500ms 用户就会觉得「卡」。

瓶颈在哪里

我这些年调启动性能,发现痛点主要集中在下面几个环节:

阶段 典型耗时 原因
创建实例 10~50ms HandlerThread 创建、默认组件初始化
MediaSource 配置 10~200ms 如果传的是 File 还好,传 Uri 需要提取类型
prepare() 加载 Manifest 100~1500ms 网络流下载 m3u8 / mpd,DNS 耗时、带宽波动
解码器初始化(核心瓶颈) 200~800ms MediaCodec 创建慢、Surface 绑定、格式协商
第一帧渲染等待 50~300ms 需要等到第一个关键帧、音视频同步延迟

你想想看,最痛的就是 解码器初始化 和 Manifest 下载。我曾在一个短视频 App 里发现,每次启动都要重新创建解码器,纯属浪费。后来做了预热池,直接砍掉 400ms。

注意: 解码器创建耗时不可忽略。尤其是在 Android 8.0 以下机型,MediaCodec 异步模式不完善,主线程可能会被阻塞几十毫秒。我曾经因为没注意这一点,导致丢帧率飙升。

实测数据 – 以 HLS 直播为例

我在测试机(Redmi Note 8 Pro,Android 10)上用 ExoPlayer 2.18 播放一条 1080p HLS 流,抓了 20 次启动日志,平均耗时分布如下:

阶段                         平均耗时   占比
----------------------------------------------
ExoPlayer 创建                28ms     2.1%
MediaSource 构建              45ms     3.4%
prepare() 回调               210ms    15.9%
解码器初始化(视频)          412ms    31.2%
解码器初始化(音频)          156ms    11.8%
第一帧渲染                     73ms     5.5%
其他(线程调度、缓冲等)      398ms    30.1%
----------------------------------------------
总耗时:1.32s

看到没有?解码器初始化占了 43%,这就是我们要啃的硬骨头。其他项目里还有更夸张的,比如某台荣耀 9X,解码器创建花了 780ms,用户直接黑屏 1 秒多。

小经验: 如果你发现总耗时异常高,先在 prepare 之前做一次 MediaCodec.createDecoderByType() 预热。注意要在后台线程做,别堵住 UI 线程。我曾经试过,预热后解码器初始化从 500ms 降到 120ms。

当然,网络流也有坑。有一次客户抱怨启动慢,我抓包发现 Manifest 下载用了 1.8 秒,结果是 CDN 回源慢。后来加了 本地缓存 Manifest,第二次启动直接减掉 1 秒。这些技巧咱们后面章节会细聊。

好,整体轮廓就是这样。记住一句话:启动速度的核心矛盾在解码器预热和网络预加载。后面我们会一步步教你怎么「偷」时间。今天先到这儿,动手试试测一下你自己的项目启动耗时,你会发现惊喜的。