## 第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 实现流程:双生命周期分层绑定
### 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 的影响?