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销毁时才会释放内存。
咨询问题
- 该现象是否为NavHost/NavController的预期行为?
- 有无修复或解决方案可在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
相关产品推荐
相关产品推荐

