2. 播放器状态绑定:PlayerHolder、rememberPlayer、DisposableEffect

在 Jetpack Compose 中,多媒体播放器的生命周期与 Composable 的重组、屏幕旋转、后台切换等场景紧密耦合。直接创建 ExoPlayer 实例会导致资源泄漏、重复创建、播放状态丢失等问题。本章通过 PlayerHolder、rememberPlayer、DisposableEffect 三个核心抽象,构建一套可复用的播放器状态绑定方案,让播放器的创建、绑定、释放与 Composable 生命周期严格对齐。

播放器状态绑定生命周期协作图 播放器状态绑定生命周期协作图 Composable 重组入口 rememberPlayer PlayerHolder ExoPlayer 实例 DisposableEffect Lifecycle 事件 箭头方向 = 创建/绑定/监听 DisposableEffect 随重组自动清除/重建

2.1 原理剖析

  • PlayerHolder – 封装 ExoPlayer 实例与配置信息(Surface、音量、循环模式等),提供统一的获取/释放接口。通过 rememberPlayer 在 Composition 内保持单例。
  • rememberPlayer – 自定义 remember 函数,内部调用 remember { PlayerHolder(context) },确保 Compose 重组时不会重建播放器。
  • DisposableEffect – Compose 的副作用 API,在 Composable 进入组合时执行初始化,离开组合时执行清理。这里用于自动注册/注销 Lifecycle 观察者,实现播放器的暂停/恢复。
设计目标:将 ExoPlayer 的生命周期从 Composable 的意外重组中隔离,完全交由 DisposableEffect + Lifecycle 管理。避免因重组、横竖屏切换或 Fragment 重建导致的播放中断或资源泄漏。

2.2 实现流程

  1. 创建 PlayerHolder 类,内部持有 ExoPlayer 实例,提供 play()、pause()、release() 等方法。
  2. 定义 rememberPlayer() 扩展函数,签名 @Composable fun rememberPlayer(): PlayerHolder,内部调用 remember { PlayerHolder(context) }。
  3. 使用 DisposableEffect 绑定 Lifecycle 并注册观察者:收到 ON_START 事件时播放,ON_STOP 时暂停,ON_DESTROY 时释放资源。
  4. 在 Composable 末尾 添加 DisposableEffect(Unit) 块,确保清理逻辑在离开组合时准确执行。

2.3 核心代码实现

以下展示了完整的 PlayerHolder + rememberPlayer + DisposableEffect 绑定模式(基于 ExoPlayer 1.0+):

class PlayerHolder(context: Context) {
    val exoPlayer: ExoPlayer = ExoPlayer.Builder(context).build()
    private var lifecycleObserver: LifecycleEventObserver? = null

    fun registerLifecycle(lifecycle: Lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_START -> exoPlayer.play()
                Lifecycle.Event.ON_STOP  -> exoPlayer.pause()
                Lifecycle.Event.ON_DESTROY -> release()
                else -> {}
            }
        }
        lifecycle.addObserver(observer)
        lifecycleObserver = observer
    }

    fun unregisterLifecycle(lifecycle: Lifecycle) {
        lifecycleObserver?.let { lifecycle.removeObserver(it) }
    }

    fun release() {
        exoPlayer.release()
    }
}

@Composable
fun rememberPlayer(): PlayerHolder {
    val context = LocalContext.current
    return remember { PlayerHolder(context) }
}

@Composable
fun VideoPlayerScreen() {
    val playerHolder = rememberPlayer()
    val lifecycleOwner = LocalLifecycleOwner.current

    // 关键:DisposableEffect 在重组时保持注册/反注册的确定性
    DisposableEffect(lifecycleOwner) {
        val lifecycle = lifecycleOwner.lifecycle
        playerHolder.registerLifecycle(lifecycle)
        onDispose {
            playerHolder.unregisterLifecycle(lifecycle)
            playerHolder.release()
        }
    }

    // 使用 playerHolder.exoPlayer 构建 AndroidView 或 Surface
    AndroidView(factory = { context ->
        SurfaceView(context).apply {
            playerHolder.exoPlayer.setVideoSurfaceView(this)
        }
    }, modifier = Modifier.fillMaxSize())
}

2.4 调试与常见错误

错误场景现象解决方案
忘记在 DisposableEffect 中 release()退出页面后播放器仍在后台占用资源在 onDispose 块中显式调用 playerHolder.release()
rememberPlayer 返回了多个实例播放器控制混乱,多个 Surface 争抢视频源确认 remember 作用域正确,避免在循环或高阶 Composable 中定义
Lifecycle 观察者未在 onDispose 中移除内存泄漏,观察者持有 Activity 引用用 unregisterLifecycle() 对称移除
在 DisposableEffect key 中传入了可变参数每次重组都重建 DisposableEffect,导致播放器频繁释放DisposableEffect 的 key 应使用稳定引用,如 lifecycleOwner
常见陷阱:不要在 Composable 中直接调用 PlayerHolder(context).exoPlayer.play()。这会在每次重组时创建新实例,且无任何清理机制,极易导致音频残留或 ANR。

2.5 性能与稳定性优化

  • 复用 PlayerHolder – 若多个 Composable 共享同一个视频源,可将 PlayerHolder 提升到上层 ViewModel 或全局单例,配合 remember 只创建一次。
  • 延迟初始化 – 使用 derivedStateOf 或 LaunchedEffect 延迟调用 exoPlayer.prepare(),避免首次组合时阻塞 UI 线程。
  • Surface 最优策略 – 使用 TextureView 而非 SurfaceView 时,需额外处理 View 被回收后的重建,建议统一用 AndroidView 包裹,并在 DisposableEffect 中管理 Surface 的注册与反注册。
  • 低内存设备适配 – 在 onStop 事件中释放高分辨率帧缓存,onStart 时重新创建 Surface。
测试建议:使用 Compose 的 TestCoroutineDispatcher 模拟重组,验证 DisposableEffect 的 onDispose 是否在每次重组后正确执行。配合 LeakCanary 检查播放器对象是否被及时回收。