如何修复ViewModel委托及Pager.flow.cachedIn引发的内存泄漏问题
如何修复ViewModel委托及Pager.flow.cachedIn引发的内存泄漏问题
我来帮你拆解和解决这个内存泄漏问题,先从根因说起,再给你针对性的解决方案:
一、为什么会出现内存泄漏?
你遇到的问题核心是ViewModel生命周期范围不匹配 + cachedIn(viewModelScope)的持久缓存特性共同导致的:
- 当你用
by activityViewModels()获取ViewModel时,它的生命周期和Activity绑定——哪怕Fragment被销毁(比如导航离开),ViewModel还会跟着Activity一直活着。 cachedIn(viewModelScope)会把Paging的数据流缓存到ViewModel的作用域中,只要ViewModel不被销毁,这个缓存就会一直持有Room PagingSource、数据库引用,甚至间接持有Fragment的Adapter、Context等对象,最终形成泄漏链(你提供的LeakCanary日志也能看到从App组件到Room再到Coroutine Flow的引用链)。
而你换成hiltNavGraphViewModels后泄漏修复,是因为导航图级别的ViewModel会在你离开该导航图时自动销毁,ViewModel作用域被取消,缓存也就跟着释放了。
二、针对性解决方案
1. 针对Fragment场景:使用匹配的ViewModel作用域
根据你的页面导航结构,选择更贴合Fragment生命周期的ViewModel委托:
- 如果这个Fragment是独立页面,不需要和其他Fragment共享ViewModel:
把Fragment里的ViewModel委托改成Fragment级作用域:override val viewModel: DocPageListViewModel by viewModels() - 如果Fragment属于某个导航图,需要和同导航图内的页面共享ViewModel:
继续用你验证有效的导航图级委托:override val viewModel: DocPageListViewModel by hiltNavGraphViewModels(R.id.docPageListFragment)
这样ViewModel会和Fragment/导航图的生命周期绑定,当Fragment被销毁或离开导航图时,ViewModel会被自动清理,cachedIn的缓存也会随之释放,切断泄漏链。
2. 针对无Fragment的Activity场景:替换cachedIn为shareIn
对于不使用Fragment的Activity,用by viewModels()时ViewModel生命周期和Activity绑定,这时候cachedIn会一直持有缓存。你可以用shareIn替代cachedIn,实现“无订阅者时自动释放缓存”的逻辑:
修改ViewModel中的数据流定义:
val pageModelPagingDataFlow = Pager(PagingConfig(pageSize = 10, initialLoadSize = 20)) { docPageDao.getPagingSource() }.flow .shareIn( scope = viewModelScope, // 当没有订阅者时,延迟5秒停止缓存并释放资源 started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), replay = 1 // 保留最新1页数据用于快速恢复 )
这里的关键是SharingStarted.WhileSubscribed(5000):当Activity进入后台、或者没有组件订阅这个Flow时,过5秒后会自动停止缓存,释放PagingSource、数据库相关的引用,避免泄漏。
三、额外注意点
- 你在Fragment中用的
viewCollectLatestWhenStarted是正确的,它会在Fragment进入STOPPED状态时自动停止收集Flow,避免Fragment销毁后还持有订阅引用。 - 不要滥用
activityViewModels():除非确实需要跨Fragment共享ViewModel,否则优先用viewModels()或导航图级委托,避免ViewModel生命周期过长导致的资源泄漏。
内容来源于stack exchange
相关产品推荐
相关产品推荐

