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

Compose NavController内存泄漏咨询:是否为预期行为及修复方案

问题背景

直接将ComposeView添加至Activity根视图,以此方式启动包含NavHost的模态底部弹窗,出现内存泄漏问题。

示例代码

class TestFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy
                    .DisposeOnViewTreeLifecycleDestroyed,
            )
            setContent {
                LazyColumn(
                    verticalArrangement = Arrangement.Top,
                    horizontalAlignment = Alignment.CenterHorizontally,
                ) {
                    item {
                        ListItem(title = "launchBS") { launchBS() }
                    }
                    item {
                        ListItem(title = "closeBS") {
                            activity
                                ?.findViewById<ViewGroup>(android.R.id.content)
                                ?.let { viewGroup ->
                                    val viewToRemove = viewGroup.findViewWithTag<ComposeView>(BS_VIEW_TAG)
                                    viewToRemove?.disposeComposition()
                                    viewGroup.removeView(viewToRemove)
                                }
                        }
                    }
                }
            }
        }
    }

    private fun launchBS() {
        activity
            ?.findViewById<ViewGroup>(android.R.id.content)
            ?.let { viewGroup ->
                val composeView = ComposeView(viewGroup.context).apply {
                    setViewCompositionStrategy(
                            ViewCompositionStrategy.DisposeOnDetachedFromWindow
                        )
                    setContent {
                        val navController = rememberNavController()
                        NavHost(navController, startDestination = DEFAULT_DESTINATION) {
                            composable(DEFAULT_DESTINATION) {
                                Text(text = "COMPOSE BOTTOMSHEET IS DISPLAYING")
                            }
                        }
                        DisposableEffect(Unit) {
                            onDispose {
                                navController.popBackStack(DEFAULT_DESTINATION, true)
                            }
                        }
                    }
                    tag = BS_VIEW_TAG
                }
                viewGroup.addView(
                    composeView
                )
            }
    } 
}

操作步骤

  • 点击“launchBS”:向Activity根视图添加包含NavHost的ComposeView,用于ViewModel作用域管理等场景;
  • 点击“closeBS”:弹出NavController回退栈并从Activity中移除该ComposeView。

问题现象

移除ComposeView后,NavHostController实例未被释放,重复打开/关闭5次会产生5个实例,仅在Activity销毁时才会释放内存。

咨询问题

  1. 该现象是否为NavHost/NavController的预期行为?
  2. 有无修复或解决方案可在Activity存活时销毁NavController及相关NavHost实例?

问题解答

1. 是否为预期行为?

这不是NavHost/NavController的预期行为,属于使用方式不当导致的内存泄漏。

核心原因:

  • rememberNavController()会将NavController与Compose重组上下文绑定,但直接将ComposeView挂载到Activity根视图时,DisposeOnDetachedFromWindow策略仅释放Compose的Composition,并未清理NavController内部持有的Activity生命周期引用、导航图观察者等关联资源;
  • 仅调用popBackStack()只是完成导航回退操作,并未彻底解除NavController与生命周期的绑定关系,导致引用无法被GC回收。

2. 修复方案

方案一:手动清理NavController关联资源

在移除ComposeView前,主动清理NavController的生命周期绑定和内部引用:

// 修改closeBS方法
activity?.findViewById<ViewGroup>(android.R.id.content)?.let { viewGroup ->
    val viewToRemove = viewGroup.findViewWithTag<ComposeView>(BS_VIEW_TAG)
    viewToRemove?.let { composeView ->
        // 从Compose树中获取NavController实例
        val rootView = composeView.getChildAt(0)
        val navController = rootView.findNavController()
        
        navController?.apply {
            // 移除生命周期观察者
            lifecycle.removeObserver(this)
            // 清空导航图,解除页面关联
            graph.clear()
        }
        
        composeView.disposeComposition()
        viewGroup.removeView(composeView)
    }
}

方案二:绑定临时生命周期所有者

避免将NavController直接关联到Activity生命周期,而是使用临时LifecycleOwner管理,移除时销毁该生命周期:

// 修改launchBS方法
private fun launchBS() {
    activity?.findViewById<ViewGroup>(android.R.id.content)?.let { viewGroup ->
        val tempLifecycle = LifecycleRegistry(requireActivity())
        tempLifecycle.currentState = Lifecycle.State.RESUMED
        
        val composeView = ComposeView(viewGroup.context).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setViewTreeLifecycleOwner(tempLifecycle)
            setContent {
                val navController = rememberNavController()
                NavHost(navController, startDestination = DEFAULT_DESTINATION) {
                    composable(DEFAULT_DESTINATION) {
                        Text(text = "COMPOSE BOTTOMSHEET IS DISPLAYING")
                    }
                }
            }
            tag = BS_VIEW_TAG
            // 存储临时生命周期引用
            setTag(R.id.temp_lifecycle_tag, tempLifecycle)
        }
        viewGroup.addView(composeView)
    }
}

// 修改closeBS方法
private fun closeBS() {
    activity?.findViewById<ViewGroup>(android.R.id.content)?.let { viewGroup ->
        val viewToRemove = viewGroup.findViewWithTag<ComposeView>(BS_VIEW_TAG)
        viewToRemove?.let { composeView ->
            val tempLifecycle = composeView.getTag(R.id.temp_lifecycle_tag) as? LifecycleRegistry
            tempLifecycle?.currentState = Lifecycle.State.DESTROYED
            
            composeView.disposeComposition()
            viewGroup.removeView(composeView)
        }
    }
}

方案三:使用官方推荐的弹窗导航方式

放弃手动添加/移除ComposeView的方式,改用Jetpack Compose官方的ModalBottomSheetLayout结合导航组件管理弹窗,自动处理生命周期和内存回收:

// 在Fragment的setContent中实现
override fun onCreateView(...) = ComposeView(requireContext()).apply {
    setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
    setContent {
        val navController = rememberNavController()
        val bottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
        val coroutineScope = rememberCoroutineScope()
        
        ModalBottomSheetLayout(
            sheetState = bottomSheetState,
            sheetContent = {
                NavHost(navController, startDestination = DEFAULT_DESTINATION) {
                    composable(DEFAULT_DESTINATION) {
                        Column(horizontalAlignment = Alignment.CenterHorizontally) {
                            Text(text = "COMPOSE BOTTOMSHEET IS DISPLAYING")
                            Button(onClick = {
                                coroutineScope.launch { bottomSheetState.hide() }
                            }) {
                                Text("关闭")
                            }
                        }
                    }
                }
            }
        ) {
            LazyColumn(
                verticalArrangement = Arrangement.Top,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                item {
                    ListItem(title = "launchBS") {
                        coroutineScope.launch { bottomSheetState.show() }
                    }
                }
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 08:30:58