必做清单(从最影响复用到最次)
-
切源时不要 stop() / release()
直接在同一个 ExoPlayer 上 setMediaItem(...) → prepare(),这样渲染器会评估是否复用已有解码器(DecoderReuseEvaluation),很多情况下能"不断流"复用。
若你先 stop(),默认就把解码器释放了,自然每次都新建。
-
必须 stop() 的场景,考虑开启前台模式
player.setForegroundMode(true) 可在 stop() 后仍尽力保留 解码器以供复用,但内存压力更大,易出稳定性问题,要谨慎、按场景开关。
-
保持 READY 暂停而不是退回 IDLE
池里的空闲播放器维持 STATE_READY(pause() 而非 stop()),通常能保留已初始化的解码器与部分缓冲,切回播放更快。
-
别频繁换 Surface,换也要"无缝迁移"
频繁销毁/切换输出 Surface 可能导致渲染器重配甚至重建。迁移视图用:
ini
PlayerView.switchTargetView(player, oldView, newView)
// 或:
oldView.player = null
newView.player = player
-
这套做法是官方推荐的迁移姿势,尽量不打断解码器。
-
让"格式尽量一致",避免触发"必须丢弃解码器"的原因****
解码器能否复用取决于老/新 Format 的差异:MIME 变了、分辨率/旋转/色彩信息剧变、DRM 会话改变、max input size 超限等都会导致丢弃重建。
- 通过 DefaultTrackSelector 禁止跨 MIME 的自适应(避免 H.264 ↔ HEVC 导致重建):
scss
val trackSelector = DefaultTrackSelector(context).apply {
setParameters(
buildUponParameters()
.setAllowVideoMixedMimeTypeAdaptiveness(false) // 默认就是 false,显式设置更稳
.setAllowAudioMixedMimeTypeAdaptiveness(false)
.setAllowAudioMixedSampleRateAdaptiveness(false)
)
}
-
- 这能减少因"混合 MIME 自适应"造成的解码器切换。
-
DRM/明文混播要用 Placeholder DrmSession
明文片段与加密片段之间切换时,启用占位 DRM 会话可避免频繁重建解码器。
-
启用解码器回退(fallback)避免初始化失败
不是"复用",但能在主硬件解码器失败时自动选用次优解码器,减少"初始化失败→重试→重建"的抖动:
scss
val renderersFactory = DefaultRenderersFactory(context)
.setEnableDecoderFallback(true)
val player = ExoPlayer.Builder(context)
.setRenderersFactory(renderersFactory)
.setTrackSelector(trackSelector)
.build()
-
官方 API 支持该选项。
-
(Media3 新能力)试试"预热解码器 / 预渲染"
Media3 1.6.0 起提供实验性 的视频渲染器预热 (通过 DefaultRenderersFactory.experimentalSetEnableMediaCodecVideoRendererPrewarming(...) 之类接口),能在下一条 MediaItem 播放前提前起第二个视频渲染器解码 ,显著降低切换延迟。适合播放列表/连续切换场景。
-
池大小要受设备"解码器实例数"约束
很多设备硬件解码器实例有限 (有的仅 2 路 H.264/VP9)。建议池里并发活跃播放器 2~3 个,其余用"就近复用"策略。
典型切源代码(不重建解码器的姿势)
kotlin
// 复用同一个 ExoPlayer 切到新视频,避免 stop()
fun ExoPlayer.playNew(item: MediaItem, autoPlay: Boolean = true) {
clearMediaItems()
setMediaItem(item)
prepare() // 关键:直接 re-prepare 触发解码器复用评估
playWhenReady = autoPlay
}
迁移视图(例如 ViewPager/RecyclerView 复用播放器):
ini
PlayerView.switchTargetView(player, oldView, newView)
// 或手动:
oldView.player = null
newView.player = player
现场排查:看到底"为啥没复用"
加一个 AnalyticsListener,看 DecoderReuseEvaluation 的 result/discardReasons:
kotlin
player.addAnalyticsListener(object : AnalyticsListener {
override fun onVideoInputFormatChanged(
eventTime: AnalyticsListener.EventTime,
format: Format,
decoderReuseEvaluation: DecoderReuseEvaluation?
) {
Log.d("Reuse", "result=${decoderReuseEvaluation?.result} " +
"reasons=${decoderReuseEvaluation?.discardReasons}")
}
})
这样你能直观看到是 MIME 变了 、分辨率超限 、DRM 会话变动......哪条在"逼"它重建。
小结(池化想真正生效,就记住 4 句话)
- 同一实例 re-prepare,别 stop/release。
- 保持 READY 暂停;必要时前台模式保活(谨慎用)。
- 控制自适应策略,尽量不跨 MIME/大幅分辨率。
- 用 AnalyticsListener 看复用失败原因,按因下药。