Navigation常见问题与性能优化:Fragment复用、避免内存泄漏、懒加载、Navigation的性能监控

Navigation组件用起来确实爽,但用久了你会发现——坑也不少。我接手过好几个项目,都是Navigation用得飞起,结果一测性能就翻车。说白了,Navigation不是银弹,你得知道它哪些地方容易出问题,怎么优化。

今天咱们就聊聊四个核心话题:Fragment复用、内存泄漏、懒加载、性能监控。这些都是我在实战中踩过的坑,希望能帮你少走弯路。

Fragment复用:别让同一个Fragment被创建N次

先问个问题:你从A页面跳到B页面,再返回A页面,A页面被重建了几次?

如果你用的是Navigation默认行为,答案是——每次返回都会重建。这其实挺浪费的。尤其是那些加载数据多、布局复杂的页面,频繁重建会让用户感觉卡顿。

核心思路:利用Fragment的setRetainInstance(true) + ViewModel缓存,让Fragment实例不被销毁。

我个人习惯的做法是这样的:

class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 保留Fragment实例,避免旋转屏幕或返回时重建
        retainInstance = true
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 通过ViewModel缓存数据,避免重复加载
        val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
        viewModel.loadData()
    }
}

嗯,这里要注意:retainInstance在Fragment重建时确实能保留实例,但它不适用于Navigation的popBackStack场景。什么意思呢?就是你从B返回A时,A的Fragment实例其实已经被Navigation移除了。

那怎么办?我建议配合Navigation的返回栈缓存来搞:

// 在NavHostFragment中设置
val navController = findNavController()
navController.navigatorProvider.addNavigator(
    NavGraphNavigator(
        context = requireContext(),
        // 开启Fragment复用
        fragmentFactory = supportFragmentManager.fragmentFactory
    )
)

但说实话,这个方案有点重。我更推荐的做法是:用ViewModel来缓存数据,而不是缓存Fragment本身。因为Fragment实例缓存容易引发内存泄漏,而ViewModel的生命周期是跟着Navigation返回栈走的,天然适合做缓存。

我的经验:在项目中,我通常让每个Fragment对应一个ViewModel,ViewModel里用LiveData或StateFlow缓存数据。这样即使Fragment重建,数据也不会丢失。用户感觉就像页面没刷新一样流畅。

避免内存泄漏:Navigation的隐形杀手

内存泄漏这个问题,我刚开始用Navigation时根本没在意。直到有一次,线上反馈说App用久了越来越卡,一查内存——好家伙,Fragment泄漏了十几个。

为什么会这样?说白了,Navigation在管理Fragment时,如果持有外部引用没有及时释放,就会导致泄漏。常见场景有:

  • 匿名内部类持有Fragment引用:比如在Fragment里写了个Handler,但没有在onDestroy时移除回调。
  • Navigation的NavController持有Fragment引用:如果你在Fragment里持有了NavController,而NavController又间接持有了Fragment,就会形成循环引用。
  • ViewModel跨Fragment共享时未清理:多个Fragment共享同一个ViewModel,如果其中一个Fragment销毁了,但ViewModel还在,就可能泄漏。

我给大家列个避坑清单:

场景 风险等级 解决方案
Fragment中使用Handler 使用LifecycleObserver或协程替代
Fragment持有NavController引用 通过findNavController()临时获取,不要长期持有
ViewModel跨Fragment共享 在Fragment的onDestroyView中清理订阅
Fragment中注册BroadcastReceiver 在onDestroy中unregister

我曾经踩过的坑:有一次我在Fragment里用了一个单例的EventBus,注册了解析回调,但忘记在onDestroy时注销。结果Fragment被销毁后,EventBus还持有Fragment的弱引用,导致Fragment无法被GC回收。排查了整整一天才找到原因。从那以后,我所有Fragment的onDestroy里都会写一个cleanup()方法,专门清理外部引用。

这里给一个通用的清理模板:

class BaseFragment : Fragment() {
    private val cleanupJobs = mutableListOf<Job>()

    override fun onDestroyView() {
        super.onDestroyView()
        // 取消所有协程
        cleanupJobs.forEach { it.cancel() }
        cleanupJobs.clear()
        // 清理ViewModel订阅
        viewModelStore.clear()
    }

    protected fun addCleanupJob(job: Job) {
        cleanupJobs.add(job)
    }
}

懒加载:让Fragment按需加载,别一上来就全加载

Navigation默认的行为是:当你导航到某个目的地时,对应的Fragment会立即创建并执行onCreateView。但如果你的Fragment里有多个Tab或者多个子页面,一上来全加载,那性能肯定扛不住。

懒加载的核心思想是:只在Fragment可见时才加载数据,而不是在创建时就加载。

我常用的方案是利用Fragment的Lifecycle来做懒加载:

class LazyFragment : Fragment() {
    private var isDataLoaded = false

    override fun onResume() {
        super.onResume()
        if (!isDataLoaded) {
            loadData()
            isDataLoaded = true
        }
    }

    private fun loadData() {
        // 加载网络数据或本地数据
        viewModel.loadData()
    }
}

但这里有个问题:如果用户快速切换页面,onResume会被频繁调用,导致数据重复加载。我建议配合用户可见性回调来做:

class LazyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 监听Fragment的可见性变化
        viewLifecycleOwner.lifecycle.addObserver(object : LifecycleEventObserver {
            override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
                if (event == Lifecycle.Event.ON_RESUME) {
                    // Fragment对用户可见时加载
                    if (!isDataLoaded) {
                        loadData()
                        isDataLoaded = true
                    }
                }
            }
        })
    }
}

我的建议:如果你的Fragment里有多个子页面(比如ViewPager+Fragment),建议使用FragmentPagerAdapter的懒加载模式,或者直接用Compose的LazyColumn。Navigation本身不负责子页面的懒加载,你得自己控制。

Navigation的性能监控:别等用户骂了才去查

性能监控这件事,很多开发者容易忽略。但你想啊,Navigation的每一次导航都涉及Fragment的创建、布局的inflate、数据的加载,如果某个页面特别慢,用户能明显感觉到。

我一般会在项目中集成自定义的Navigation性能监控,记录每次导航的耗时:

class PerformanceNavController(context: Context, navHostFragment: NavHostFragment) :
    NavController(context) {

    private val navigationTimestamps = mutableMapOf<Int, Long>()

    override fun navigate(
        destinationId: Int,
        args: Bundle?,
        navOptions: NavOptions?,
        navigatorExtras: Navigator.Extras?
    ) {
        val startTime = System.currentTimeMillis()
        navigationTimestamps[destinationId] = startTime

        super.navigate(destinationId, args, navOptions, navigatorExtras)

        // 在Fragment的onResume时计算耗时
        // 这里可以通过回调或者监听器来获取
    }

    fun getNavigationDuration(destinationId: Int): Long? {
        val startTime = navigationTimestamps[destinationId] ?: return null
        return System.currentTimeMillis() - startTime
    }
}

当然,这个方案比较简陋。更专业的做法是使用Android的Trace工具或者自定义的AOP切面来监控:

// 使用AspectJ或ASM在编译期织入监控代码
@Around("execution(* androidx.navigation.NavController.navigate(..))")
fun aroundNavigate(joinPoint: ProceedingJoinPoint) {
    val start = System.nanoTime()
    joinPoint.proceed()
    val duration = (System.nanoTime() - start) / 1_000_000 // 毫秒
    Log.d("NavPerformance", "导航耗时: $duration ms")
    // 上报到性能监控平台
}

核心指标:我建议重点关注三个指标:

  • 导航耗时:从调用navigate到Fragment的onResume执行完毕
  • Fragment创建耗时:onCreate + onCreateView + onViewCreated的总时间
  • 数据加载耗时:ViewModel中数据加载完成的时间

如果某个页面的导航耗时超过200ms,就需要优化了。

另外,别忘了监控返回栈的深度。如果用户连续跳转了十几个页面,返回栈会变得很深,内存占用也会增加。我建议在用户跳转超过5层时,弹出提示或自动清理历史栈:

navController.addOnDestinationChangedListener { _, destination, _ ->
    val backStackSize = navController.backStack.size
    if (backStackSize > 5) {
        // 清理中间页面,只保留首页和当前页
        navController.popBackStack(R.id.home, false)
    }
}

注意:清理返回栈要谨慎,别把用户正在操作的页面给清了。我一般只在用户进入深层页面(比如设置页的某个子页)时做清理,不影响主流程。

总结一下

Navigation的性能优化,说白了就是三件事:减少不必要的创建、避免泄漏、按需加载。Fragment复用能减少页面重建的开销,内存泄漏排查能保证App长期运行的稳定性,懒加载能提升首屏速度,性能监控能让你提前发现问题。

我个人觉得,Navigation本身的设计已经很优秀了,但它的默认行为不一定适合所有场景。你需要根据自己项目的实际情况,做针对性的优化。别等到用户反馈卡顿才去查,那时候就晚了。

嗯,今天就聊到这儿。如果你在实际项目中遇到其他Navigation的问题,欢迎交流。


公众号:蓝海资料掘金营,微信deep3321