## 第3章 生命周期绑定与优化:Compose DisposableEffect、LifecycleObserver、暂停/继续播放、避免内存泄漏 ### 3.1 本章知识点结构 | 核心概念 | 作用 | 关键 API / 组件 | |----------|------|------------------| | **Compose DisposableEffect** | 把 Compose 副作用与重组生命周期绑定,在 Composable 进入组合时注册、离开时自动清理 | `DisposableEffect(keys) { onDispose { … } }` | | **LifecycleObserver** | 观察 Activity/Fragment 生命周期状态变化,实现播放控制 | `DefaultLifecycleObserver` + `onStart()`/`onStop()` | | **暂停/继续播放** | 利用 Lifecycle.State 判断可见性和前台状态,调用 `player.playWhenReady` 和 `player.pause()` | `lifecycle.addObserver(observer)` | | **避免内存泄漏** | 及时释放 ExoPlayer 实例、Listener 注册、协程作用域等 | `player.release()`、取消协程 Job、解绑 LifecycleObserver | ### 3.2 原理:为什么需要生命周期绑定 ExoPlayer 实例持有媒体解码器、渲染线程、网络连接等重型资源。如果 Compose 可组合项在重组中重复创建播放器而不销毁,或 Activity 进入后台时仍保持解码器活跃,会导致内存泄漏和电量浪费。Compose 的生命周期管理需要同时处理: - **Compose 重组生命周期**:`remember` + `DisposableEffect` 保证 Player 实例只创建一次,并在 Composable 退出时清理。 - **Android 组件生命周期**:`LifecycleObserver` 感知可见性变化,避免后台继续播放视频/音频。 两者的错误结合是生产环境常见的稳定性问题根源。 ### 3.3 实现流程:双生命周期分层绑定 生命周期绑定策略流程图 Composable 进入组合 @Composable VideoPlayer() remember 创建 ExoPlayer 实例 exoPlayer = remember { ExoPlayer.Builder(context).build() } DisposableEffect 注册 LifecycleObserver lifecycle.addObserver(observer) / onDispose { lifecycle.removeObserver } Lifecycle.State 驱动播放控制 onStart → playWhenReady = true | onStop → player.pause() onDispose 释放资源 player.release() + 取消协程 Job 要点:两个生命周期(Compose 重组 + Android 组件)分层治理,DisposableEffect 负责注册/反注册,LifecycleObserver 负责播放状态同步。 ### 3.4 核心代码实现 #### 3.4.1 基础播放 Composable(含 DisposableEffect)
@Composable
fun VideoPlayer(
    mediaItem: MediaItem,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val lifecycle = lifecycleOwner.lifecycle

    // 1. 创建 Player 实例(仅一次)
    val player = remember {
        ExoPlayer.Builder(context)
            .setMediaSourceFactory(
                DefaultMediaSourceFactory(context)
                    .setDataSourceFactory(DefaultHttpDataSource.Factory())
            )
            .build()
            .apply {
                setMediaItem(mediaItem)
                prepare()
                playWhenReady = true
            }
    }

    // 2. DisposableEffect 绑定生命周期
    DisposableEffect(lifecycle) {
        // 创建 LifecycleObserver
        val observer = object : DefaultLifecycleObserver {
            override fun onStart(owner: LifecycleOwner) {
                player.playWhenReady = true
            }

            override fun onStop(owner: LifecycleOwner) {
                player.playWhenReady = false
                player.pause()
            }
        }

        lifecycle.addObserver(observer)

        // 3. 清理:移除 Observer 并释放 Player
        onDispose {
            lifecycle.removeObserver(observer)
            player.release()  // 释放解码器、渲染线程、Surface
        }
    }

    // 4. 显示 Surface
    AndroidView(
        modifier = modifier,
        factory = { ctx ->
            SurfaceView(ctx).apply {
                player.setVideoSurfaceView(this)
            }
        }
    )
}
#### 3.4.2 增强版:支持后台保持播放(可选配置)
// 主界面 Composable 调用时传递 lifecycleOwner
@Composable
fun StableVideoPlayer(
    mediaItem: MediaItem,
    keepInBackground: Boolean = false  // 是否在后台继续播放音频
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val lifecycle = lifecycleOwner.lifecycle

    val player = remember {
        ExoPlayer.Builder(context)
            .setAudioAttributes(AudioAttributes.DEFAULT, true) // 允许后台音频
            .build()
            .apply {
                setMediaItem(mediaItem)
                prepare()
                playWhenReady = true
            }
    }

    DisposableEffect(lifecycle, keepInBackground) {
        val observer = object : DefaultLifecycleObserver {
            override fun onStop(owner: LifecycleOwner) {
                if (!keepInBackground) {
                    player.pause()
                    player.playWhenReady = false
                }
                // keepInBackground = true 时不暂停,保持音频
            }
        }
        lifecycle.addObserver(observer)

        onDispose {
            lifecycle.removeObserver(observer)
            // 注意:不要在这里 release,因为 Activity 可能只是回到后台
            // 更好的做法是在 Activity.onDestroy 回调中释放
        }
    }

    // 在 Activity.onDestroy 时最终释放
    DisposableEffect(Unit) {
        onDispose {
            if (player.playbackState != Player.STATE_IDLE) {
                player.release()
            }
        }
    }

    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { ctx ->
            TextureView(ctx).apply {
                player.setVideoTextureView(this)
            }
        }
    )
}
### 3.5 调试:生命周期日志观察 在开发阶段添加日志可以验证生命周期绑定是否按预期执行:
class LifecycleLoggingObserver(private val tag: String = "PlayerLifecycle") : DefaultLifecycleObserver {
    override fun onStart(owner: LifecycleOwner) {
        Log.d(tag, "onStart → 恢复播放")
    }
    override fun onStop(owner: LifecycleOwner) {
        Log.d(tag, "onStop → 暂停播放")
    }
    override fun onDestroy(owner: LifecycleOwner) {
        Log.d(tag, "onDestroy → 准备释放资源")
    }
}
运行 `adb logcat -s PlayerLifecycle` 过滤日志。如果发现 `onDestroy` 未被调用而 `player.release()` 已执行,说明 DisposableEffect 可能在错误的生命周期节点触发了清理——通常是因为重组导致 key 变化重新触发了 `onDispose`。 ### 3.6 常见错误与避坑指南 | 错误场景 | 原因 | 正确做法 | |----------|------|----------| | **重复创建 Player** | 忘记使用 `remember`,导致每次重组都 new 一个实例 | 将 Player 实例放入 `remember` 中 | | **DisposableEffect 中 key 设置不当** | 使用可变状态作为 key,每次值变化触发清理和重建 | 使用稳定的 key:`lifecycle` 或 `Unit` | | **在 onDispose 中遗漏 player.release()** | 只移除了 Observer,Player 仍保持活跃 | 在 `onDispose` 中显式调用 `player.release()` | | **未移除 LifecycleObserver** | 只 addObserver 不 remove,导致内存泄漏 | 在 `onDispose` 中 `lifecycle.removeObserver(observer)` | | **在后台 Activity 中仍播放视频** | 未监听 onStop,或监听后未调用 pause() | 使用 DefaultLifecycleObserver.onStop 暂停 | | **在 SurfaceView 重建时未重新绑定** | 窗口销毁后 Surface 无效,仍调用 setVideoSurfaceView | 使用 `setVideoSurface` 与 SurfaceCallback 或使用 TextureView | ### 3.7 性能与稳定性优化建议 - **复用 Player 实例**:如果页面内多次切换视频,可设计单例或 ViewModel 持有 Player,避免反复 `build` / `release`。 - **延迟初始化**:不要在 Composable 的顶层立即 `prepare()`,可以等到用户点击播放时才调用。 - **使用 WeakReference**:如果 Observer 持有 Activity 或 Context 引用,容易导致内存泄漏,使用 `WeakRef` 包装。 - **协程作用域清理**:如果 Player 播放时启用了协程(例如加载缓存、监听进度),使用 `rememberCoroutineScope()` 并在 `onDispose` 中 `scope.cancel()`。
@Composable
fun SafeVideoPlayer(mediaItem: MediaItem) {
    val scope = rememberCoroutineScope()
    val player = remember { ExoPlayer.Builder(LocalContext.current).build() }

    DisposableEffect(Unit) {
        onDispose {
            scope.cancel()
            player.release()
        }
    }
    // ...
}
### 3.8 部署检查清单 - [ ] 是否所有 Player 实例都通过 `remember` 或 ViewModel 缓存? - [ ] DisposableEffect 的 `onDispose` 中是否同时移除了 Observer 并调用了 `player.release()`? - [ ] 是否使用了 `DefaultLifecycleObserver` 而非已废弃的 `LifecycleObserver` 接口? - [ ] 后台继续播放的场景是否显式配置了 `keepInBackground` 开关? - [ ] 是否在 `onStop` 中释放了 Surface 引用?TextureView 和 SurfaceView 的 Surface 生命周期是否独立考虑了?(提示:SurfaceView 的 Surface 可能在 Activity 可见时被销毁重建) - [ ] 单元测试中是否 mock 了 Lifecycle 行为,验证 onStart/onStop 对 playWhenReady 的影响?