先把结论放在前面:Fragment 与 ViewModel 的作用域问题,本质是"状态该活多久"这一个设计决策。 Fragment 有视图生命周期(可随时重建)与实例生命周期(随返回栈存活)两段,ViewModel 有 Fragment 级、Activity 级、NavBackStackEntry 级三档。选错档位的后果不是报错,而是状态串页、内存常驻、或者参数丢失这三类难查的线上问题。
这题的区分度:只答"viewModels() 是 Fragment 级、activityViewModels() 是 Activity 级"属于背定义;能讲清两段生命周期、三档作用域的适用边界、以及"为什么 VM 里不能持 Context",就是有实践的答案。
两段生命周期与三档作用域
// 三档作用域的写法与实际边界
class DetailFragment : Fragment(R.layout.f_detail) {
// ① Fragment 级:与 Fragment 实例同生共死
private val detailVm: DetailViewModel by viewModels()
// ② Activity 级:与宿主 Activity 同生共死,跨 Fragment 共享
private val userVm: UserViewModel by activityViewModels()
// ③ NavBackStackEntry 级:Navigation 场景的"页面级"作用域
private val navVm: FormViewModel by viewModels(
ownerProducer = {
requireParentFragment() },
factoryProducer = {
defaultViewModelProviderFactory }
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 关键:收集 Flow 必须绑 viewLifecycleOwner,而不是 Fragment 本身
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
detailVm.uiState.collect {
render(it) }
}
}
}
}
第③档在 Navigation 组件里写作 hiltNavGraphViewModels 或通过 findNavController().getBackStackEntry(...) 取。 它的存在理由是:Navigation 里每个目的地(destination)本质是一个 Fragment,它自己的 ViewModel 就是"页面级"的——从 A 页面跳到 B,A 的返回栈条目仍在,A 的 VM 存活,这与 viewModels() 的 Fragment 级语义一致,只是主体换成了 NavBackStackEntry。
两段生命周期的关键差别在这里体现:Fragment 的实例在导航离开后(未被 remove)仍然存在,但它的 View 会被销毁(onDestroyView)。所以 VM 活得更久、View 活得更短,两者的绑定必须走 viewLifecycleOwner。这一条是本篇的核心,也是最常被追问的地方。
三档作用域的选型判据
| 档位 | 生命周期 | 适用状态 | 误用的后果 |
|---|---|---|---|
viewModels() |
Fragment 实例 | 页面私有:表单草稿、列表分页状态 | 放到宿主级会让返回后残留旧状态 |
activityViewModels() |
Activity 实例 | 跨页共享:登录态、用户信息、购物车 | 放页面私有状态会导致切页后互相污染 |
| NavBackStackEntry 级 | 返回栈条目 | Navigation 的页面级状态 | 与 Activity 级混用会导致返回时状态错乱 |
// 反模式:把页面私有状态放进 Activity 级 VM
class MainActivity : AppCompatActivity() {
private val vm: MainViewModel by viewModels() // 列表滚动位置、筛选条件放在这里
}
// 后果:用户 A 页设了筛选 → 跳到 B → 返回 A,筛选条件被 B 改掉了
判据一句话:状态的归属方是谁,就挂在谁的层级上。 状态属于某个页面 → Fragment 级;状态属于整个 App 会话 → Activity 级;状态属于一个导航目的地 → 返回栈条目级。
为什么 VM 不能持有 View 或 Activity
// 错误写法一:持有 Activity Context
class BadVm(private val ctx: Activity) : ViewModel() {
fun start() {
someDialog(ctx) } // 泄漏 Activity
}
// 错误写法二:持有 View
class BadVm2(private val v: View) : ViewModel()
// 正确:需要 Context 时用 Application
class GoodVm(app: Application) : AndroidViewModel(app) {
fun label() = getApplication<Application>().getString(R.string.app_name)
}
泄漏链路的完整形态是:GC Root(单例/静态/ViewModelStore)→ ViewModel → Activity → DecorView → 整个视图树。 旋转屏幕时 Activity 被重建,旧 Activity 被 ViewModel 引用着,于是泄漏。
AndroidViewModel 为什么可以用? 因为它持有的是 Application(进程级单例),与任何具体 Activity 无关。但仍要注意别把 View 传进去。
最常见的坑是
第一层坑:把页面私有状态放进宿主级 ViewModel,返回后残留旧状态。
现场:详情页把筛选条件写进了 Activity 级 VM,用户从详情返回列表,发现列表被过滤了。根因是状态的归属方判断错。规范是先问"这个状态跟着哪个页面走",跟着页面的就挂在页面级。
第二层坑:收集 Flow 时用了 Fragment 作用域而不是 viewLifecycleOwner,视图销毁后仍在更新 UI。
// 错误写法
lifecycleScope.launch {
vm.uiState.collect {
binding.text.text = it } }
// binding 在 onDestroyView 后已失效 → 空指针或内存泄漏
根因是 Fragment 的 lifecycleScope 活到 onDestroy,而 binding 活到 onDestroyView。规范是用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle(STARTED) 组合,它同时保证了"视图存在"与"界面可见才收集"两个条件。
第三层坑:返回栈中的 Fragment 未销毁实例,其 ViewModel 同步存活,内存常驻。
场景:导航栈里压了 5 层,每层都持有一个大列表的 VM(缓存了几 MB 数据),用户一直不返回,内存持续增长。规范是① 用 navController.popUpTo 及时清理返回栈;② 对列表类 VM 做数据上限控制;③ 用 SavedStateHandle 只存恢复状态所需的小字段,不把大列表塞进去。
这题在面试里怎么问、怎么答
问:Fragment 和 ViewModel 的生命周期是怎么绑定的?
答:Fragment 有两段——实例生命周期(onCreate 到 onDestroy,导航离开时不销毁)与视图生命周期(onCreateView 到 onDestroyView,视图重建时销毁)。by viewModels() 获取的 VM 绑在 FragmentStore 上,跟随实例生命周期;VM 持有的 ViewModelStore 在配置变更时通过 NonConfigurationInstance 保留,所以旋转屏幕不丢。关键补充:UI 相关的收集必须走 viewLifecycleOwner,因为它活到 onDestroyView。
问:viewModels()、activityViewModels()、navGraphViewModels() 有什么区别?
答:viewModels() 绑 Fragment 实例,页面私有状态用它;activityViewModels() 绑 Activity 实例,跨 Fragment 共享状态用它(登录态、购物车);Navigation 场景下 NavBackStackEntry 级作用域对应"目的地级"VM,与页面同生共死但主体是返回栈条目而非 Fragment 实例。选型判据是"状态的归属方是谁,就挂在谁的层级",选错会导致状态串页或残留。
问:为什么 ViewModel 里不能持有 Activity 或 View?
答:因为会造成内存泄漏。VM 存放在 ViewModelStore 里,而这个 Store 在配置变更时被静态的 NonConfigurationInstance 引用,等于 VM 活得比 Activity 长。若 VM 再持有 Activity,泄漏链就是 GC Root → ViewModel → Activity → 视图树。需要 Context 时用 AndroidViewModel 持 Application,且仍不得持有 View。再补一句:LeakCanary 报这类泄漏时会精确给出这条链,能自己顺着读出来是加分点。
问:repeatOnLifecycle(STARTED) 解决什么问题?
答:解决"界面不可见时仍在收集数据"。lifecycleScope.launch 本身不感知可见性,收集协程会一直运行;repeatOnLifecycle 在 ON_STOP 时取消内部协程、ON_START 时重新启动,所以每次回到前台都会拿到最新状态。它带来的正确性收益是"界面可见期间的数据始终是最新的",代价是配置变化时会重跑一次收集块,所以块里不能放一次性逻辑。
落到项目里怎么做
一条能写进规范的红线:所有 Flow 收集必须使用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle(STARTED) 组合,且 ViewModel 不得持有任何 View 或 Activity 引用。 这一条能拦住本篇三类最常见的问题。
配套实践三条:① 把"状态归属方"写进 VM 类的 KDoc 注释,明确这个 VM 由哪一层持有、承载哪类状态,review 时直接对照;② 给返回栈深度设上限(导航图里限制层级数,超过则 popUpTo),避免深栈内存常驻;③ 用 LeakCanary 在 Debug 包常驻,把泄漏发现提前到开发期而不是用户反馈里。
给正在准备面试的你
这题的答法要落到"状态该活多久是唯一的设计决策,两段生命周期 + 三档作用域是它的两种表达"这条主线。推荐这条线:先讲 Fragment 两段生命周期与 VM 的绑定位置 → 给三档作用域的选型判据(状态归属方是谁)→ 讲为什么 VM 不能持 Activity/View 与完整泄漏链 → 讲 viewLifecycleOwner + repeatOnLifecycle 的两个条件 → 补三个坑 → 收在"收集规范"。能被追问"旋转屏幕时 Fragment 的 View 和实例哪个活"并答出"实例活、View 重建",说明真做过状态管理;能主动画出 GC Root → ViewModel → Activity → 视图树 这条泄漏链,说明处理过泄漏排查。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:组件与-UI-阶段通关地图:从生命周期到-Compose
下一篇预告:NestedScrollView-与-CoordinatorLayout:联动滚动机制
有任何问题欢迎在评论区留言交流。