</>FUNNYGUYZ

Lifecycle、ViewModel 与 StateFlow

#Lifecycle#ViewModel#StateFlow
← 学习资料优先级:核心

核心原理讲解

Lifecycle 观察者模式是怎么实现的?

核心实现依托于三个角色:LifecycleOwner(生命周期持有者,如 Activity/Fragment)、LifecycleObserver(观察者)和 LifecycleRegistry(分发注册表)。

图片 04-1:LifecycleOwner 与 LifecycleRegistry 观察者状态对齐状态机

  • 如何同步状态LifecycleRegistry 内部维护了一个状态机(State 状态与 Event 事件)。当宿主生命周期变化时(比如 Activity 走到了 onStart),会通过关联的 ReportFragmentLifecycleRegistry 发送一个 ON_STARTEvent
  • LifecycleRegistry 接收到 Event 后,会推进自身的 State(例如从 CREATED 变为 STARTED),并遍历持有的观察者列表,逐个触发回调。它能保证不管观察者是在什么时候注册进来的,都能自动向后对齐到宿主当前的最新状态。

ViewModel 重建保活的机制

ViewModel 为什么能在屏幕旋转、语言切换等配置重建(Configuration Changes)后依然存活?

图片 04-2:Activity 配置变更重建与 ViewModelStore 暂存恢复机制图

  • 核心秘密:系统在销毁因配置变更重建的 Activity 时,会先调用 retainNonConfigurationInstances() 将内部的 ViewModelStore 存起来。
  • 重建恢复:当新 Activity 实例重建并创建 ViewModelProvider 时,会通过 getLastNonConfigurationInstance() 拿回刚才暂存的 ViewModelStore,从中取出原本就存在的 ViewModel 实例。
  • 真正销毁:只有当 Activity 真正调用了 finish() 退出生命周期(或者 Fragment 真正被 Pop 出栈)时,宿主才会告诉 ViewModelStore 调用所有 ViewModel 的 onCleared() 来彻底释放资源。

StateFlow 在 MVI 中的订阅防漏与生命周期感知

在 MVI 中,View 订阅 ViewModel 中的单一不可变状态 StateFlow。 如果直接在 lifecycleScope.launch 里收集(collect)数据,当 App 被切到后台(处于 STOPPED 状态)时,底层的 Flow 收集协程依然在活跃,生产者如果继续推送数据,协程依然会处理,这会造成不必要的 CPU 开销和潜在的 UI 异常。

图片 04-3:repeatOnLifecycle(STARTED) 自动收集与后台自动取消协程生命周期图

  • 解决办法:使用 repeatOnLifecycle(Lifecycle.State.STARTED)。它是一个挂起函数,当生命周期进入 STARTED 时自动启动协程进行 Flow 收集;一旦生命周期低于 STARTED(比如切到后台变成 STOPPED),它会直接取消(cancel)收集协程;回到前台后又会重新创建协程开始收集。这比旧的 launchWhenStarted 更加安全和省电。

工程注意事项

  • [!IMPORTANT] Fragment 生命周期订阅的坑(致命高频):在 Fragment 中观察 LiveData 或收集 StateFlow 时,lifecycleOwner 参数传入 this 还是 viewLifecycleOwner必须用 viewLifecycleOwner!因为 Fragment 的视图销毁时(onDestroyView),Fragment 实例可能还活着(没有 onDestroy),如果用了 this,此时 LiveData 观察者没有被销毁,当数据变化时会尝试去操作已经销毁的 View 导致直接崩溃。viewLifecycleOwner 的生命周期与 Fragment 的 View 绑定,在 onDestroyView 时会自动断开观察。

  • [!NOTE] MVI 状态 copy 的开销:MVI 强调 State 不可变,状态变化必须 copy 整个对象。如果 UI 很复杂,高频变化时会有大量的对象创建和 GC 压力。需要在 ViewModel 中合理分层,利用 Flow 的 distinctUntilChanged() 对状态字段进行去重,防止不相关的子 View 频繁被动刷新。