Jetpack Compose中ExoPlayer PlayerView返回时UI泄漏问题求助
Jetpack Compose中ExoPlayer PlayerView返回时残留/泄漏问题
在Jetpack Compose页面中使用ExoPlayer的PlayerView时,按下返回键或调用navController.popBackStack()返回上一屏,PlayerView会残留到上一屏,几秒后才消失。
问题页面代码
@OptIn(ExperimentalMaterialApi::class) @Stateless @Composable fun HelpVideo(@RawRes videoResource: Int,onNavIconClicked: () -> Unit,@StringRes guideTitle: Int) { ApplyStatusNavBarColor() MaintenanceAppScaffold(topAppBar = { CommonAppBar( text = R.string.soundassist_update_interrupted_video, isInfoButton = false, isBackButton = false ) }) { val context = LocalContext.current val exoPlayer = remember { ExoPlayer.Builder(context).build().apply { val videoUri = RawResourceDataSource.buildRawResourceUri(videoResource) val mediaItem = MediaItem.fromUri(videoUri) addMediaItem(mediaItem) prepare() playWhenReady = true } } LaunchedEffect(exoPlayer) { exoPlayer.awaitVideoFinish() onNavIconClicked() } AndroidView( modifier = Modifier.fillMaxSize(), factory = { StyledPlayerView(context).apply { player = exoPlayer layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams .MATCH_PARENT, ViewGroup.LayoutParams .MATCH_PARENT ) useController = false } } ) DisposableEffect(exoPlayer) { onDispose { exoPlayer.release() } } }}
问题详情
返回上一屏时:
- 无论按下返回键还是调用
navController.popBackStack(),PlayerView都会在上一屏显示几秒后才消失。
已尝试方案
- 使用
navController.popBackStack()——问题依旧存在。 - 临时方案:调用
navController.navigate(GoToPreviousScreen),但引发其他导航问题,非理想解决方式。
疑问与需求
为何返回时PlayerView会出现这种行为?如何在保留正确导航行为、避免navigate()这类临时方案的前提下修复该问题?
附加信息
使用Jetpack Compose搭配ExoPlayer进行视频播放,问题似乎与AndroidView中PlayerView的生命周期或销毁逻辑相关。
问题原因分析
这个问题的核心是AndroidView的销毁时机与ExoPlayer的释放逻辑不匹配,结合Compose导航的视图切换机制共同导致:
- Compose执行
popBackStack()时,当前可组合项不会立即从视图树移除,需经历重组和视图销毁流程,存在延迟。 - PlayerView作为原生View,其Surface渲染层级独立于Compose视图树,即便Composable标记为销毁,原生Surface可能仍短暂保留在Window中,直到ExoPlayer完全释放或View被彻底detach。
- 当前代码仅在
DisposableEffect中释放ExoPlayer,未明确处理PlayerView与Player的解绑、PlayerView的销毁逻辑,导致视图残留。
修复方案
方案1:在AndroidView中处理销毁时的解绑与移除
通过AndroidView的update块,监听Composable的销毁状态,主动解绑Player并从父布局移除PlayerView:
AndroidView( modifier = Modifier.fillMaxSize(), factory = { StyledPlayerView(context).apply { player = exoPlayer layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) useController = false } }, update = { playerView -> // 检测当前Composable是否即将销毁 val isDisposed = !currentComposer.currentRecomposeScope.isValid if (isDisposed) { playerView.player = null // 从父布局移除PlayerView (playerView.parent as? ViewGroup)?.removeView(playerView) } } )
方案2:优化DisposableEffect的释放逻辑
在DisposableEffect的onDispose中,先停止播放、解绑Player,再释放ExoPlayer,确保资源清理顺序:
DisposableEffect(exoPlayer) { onDispose { exoPlayer.stop() // 解绑PlayerView与Player (exoPlayer.view as? StyledPlayerView)?.player = null exoPlayer.release() } }
方案3:结合生命周期感知提前清理
利用LocalLifecycleOwner监听页面生命周期,在页面进入DESTROYED状态时立即处理资源:
val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(lifecycleOwner) { lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.DESTROYED) { exoPlayer.stop() (exoPlayer.view as? StyledPlayerView)?.player = null exoPlayer.release() } }
额外优化建议
- 避免依赖
awaitVideoFinish()触发导航的逻辑,视频播放完成后直接调用navController.popBackStack()返回,减少生命周期冲突。 - 确保
onNavIconClicked()内部是执行popBackStack()而非navigate(),避免额外的导航栈问题。
内容的提问来源于stack exchange,提问作者JaTiN Gujjar
相关产品推荐
相关产品推荐

