You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose切换页面时ExoPlayer销毁延迟问题排查

Jetpack Compose切换页面时ExoPlayer画面残留问题分析与修复

可能的原因

  • 生命周期监听逻辑错误:ExoPlayerLifecycleOwner中把AndroidView作为DisposableEffect的key,导致播放器的生命周期绑定到View的创建销毁,而非页面生命周期。页面跳转时,原页面进入ON_PAUSE仅调用exoPlayer.stop(),但播放器View未立即隐藏,加上Compose重组滞后,画面残留到新页面。
  • Compose重组与原生View销毁延迟:Compose的UI重组是异步的,页面跳转时VideoPlayer的isPlaying状态无法立即更新,原生StyledPlayerView销毁有延迟,过渡阶段画面会短暂显示在新页面。
  • 播放器资源释放不及时:ExoPlayer.release()仅在DisposableEffect的onDispose中调用,若DisposableEffect因key设置问题未及时触发,播放器持续持有资源,View无法及时销毁。

修复方案

1. 修正生命周期监听逻辑

将ExoPlayerLifecycleOwner的DisposableEffectkey改为页面生命周期所有者和播放器实例,确保生命周期变化时正确控制播放器,并在页面销毁时释放资源:

@Composable
internal fun ExoPlayerLifecycleOwner(exoPlayer: ExoPlayer) {
    val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)

    DisposableEffect(lifecycleOwner.value, exoPlayer) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> {
                    if (exoPlayer.playWhenReady) exoPlayer.play()
                }
                Lifecycle.Event.ON_PAUSE -> exoPlayer.pause()
                Lifecycle.Event.ON_STOP -> exoPlayer.stop()
                Lifecycle.Event.ON_DESTROY -> exoPlayer.release()
                else -> {}
            }
        }

        val lifecycle = lifecycleOwner.value.lifecycle
        lifecycle.addObserver(observer)

        onDispose {
            lifecycle.removeObserver(observer)
            if (exoPlayer.isPlaying) exoPlayer.stop()
            exoPlayer.release()
        }
    }

    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            StyledPlayerView(context).apply {
                resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                player = exoPlayer
            }
        }
    )
}

2. 监听页面离开状态,强制重置播放状态

在VideoPlayer中添加页面生命周期监听,进入ON_PAUSE时立即把isPlaying设为false,让Compose快速切换到封面图:

@Composable
private fun VideoPlayer(videoUrl: String) {
    val context = LocalContext.current
    var isPlaying by remember { mutableStateOf(false) }
    val videoUri = Uri.parse(videoUrl)
    val lifecycleOwner = LocalLifecycleOwner.current

    val cacheDataSource = CacheDataSourceFactory().createCacheDataSource()

    val exoPlayer = remember {
        ExoPlayer.Builder(context).build().apply {
            val mediaSource = ProgressiveMediaSource.Factory(cacheDataSource)
                .createMediaSource(MediaItem.fromUri(videoUri))
            setMediaSource(mediaSource)
            playWhenReady = false
            prepare()
        }
    }

    // 页面离开时重置播放状态
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_PAUSE) isPlaying = false
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    LaunchedEffect(isPlaying) {
        if (isPlaying) {
            exoPlayer.seekToDefaultPosition()
            exoPlayer.play()
        } else {
            exoPlayer.pause()
        }
    }

    // 原有UI逻辑保持不变
    if (isPlaying) {
        Box(
            modifier = Modifier
                .height(Constants.dimensions.d220)
                .padding(vertical = SynergyTheme.dimensions.x2_5)
                .clip(SynergyTheme.shapes.medium),
        ) {
            ExoPlayerLifecycleOwner(exoPlayer)
        }
    } else {
        Box {
            ImageFromSource(
                iconSource = IconSource.FromResource(
                    resourceId = R.drawable.about_synergy,
                    contentDescription = null,
                ),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = SynergyTheme.dimensions.x2_5)
                    .clip(SynergyTheme.shapes.medium),
                contentScale = ContentScale.FillWidth,
            )
            Image(
                modifier = Modifier
                    .size(Constants.dimensions.d56)
                    .align(Alignment.Center)
                    .clickable { isPlaying = true },
                painter = painterResource(id = R.drawable.about_play_video),
                contentDescription = null,
                contentScale = ContentScale.Crop,
            )
        }
    }
}

3. 确保页面切换时View及时隐藏

通过上述生命周期监听,页面跳转时isPlaying会被立即设为false,Compose会快速切换到封面图,避免播放器View在过渡阶段显示。

内容的提问来源于stack exchange,提问作者Ruslan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 09:22:04