Android Jetpack - 2 ViewModel

156 阅读22分钟

1. 为什么需要 ViewModel

1.1 传统开发中的四大痛点

痛点表现后果
数据丢失屏幕旋转、语言切换时 Activity 重建,成员变量被清空用户输入、滚动位置、未提交表单全部丢失
内存泄漏异步任务持有 Activity 引用,Activity 已销毁但任务未结束无法被 GC 回收,OOM 风险
职责混乱Activity 既管 UI 又管数据与业务难以维护,违反单一职责
难以测试业务逻辑与 Context、View 强耦合只能做仪器化测试,成本高

1.2 ViewModel 的定位

一句话:ViewModel 是与界面生命周期解耦的 UI 状态容器,在配置变更时不会被销毁,只在 Owner(即提供 ViewModelStore 的宿主,通常是 Activity、Fragment 或 NavBackStackEntry)因非配置变更而被销毁时才会被清除。
对应 1.1 的痛点:ViewModel 直接解决配置变更导致的数据丢失,并有利于职责分离与单测;内存泄漏需正确使用(不持 Activity、用 viewModelScope 等)才能避免。

flowchart TB
    subgraph 传统方式
        A1[Activity] --> A2[成员变量]
        A2 -->|旋转屏幕| A3[数据丢失]
    end
    
    subgraph ViewModel方式
        B1[Activity] --> B2[ViewModel]
        B2 --> B3[ViewModelStore]
        B3 -->|旋转屏幕| B4[存到 Record<br/>新 Activity 取回]
        B4 -->|新 Activity| B2
    end

2. ViewModel 使用指南

2.1 基本用法:状态驱动 UI

核心思路:ViewModel 持有「UI 状态」,界面只做「观察 + 渲染 + 转发用户操作」。下面示例中的 viewModelScope 会在 ViewModel 被清除时自动取消其下协程,避免泄漏。

// ViewModel:状态 + 加载
class UserProfileViewModel(private val repo: UserRepository) : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    fun load() { viewModelScope.launch { _uiState.update { it.copy(user = repo.getUser()) } } }  // update 为 StateFlow 扩展,需 import kotlinx.coroutines.flow.update
}

// Activity/Fragment:观察 + 转发点击
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { /* 根据 it 更新 UI */ } }
}
refreshButton.setOnClickListener { viewModel.load() }  // 仅转发,不写业务

2.2 四种创建方式对比

flowchart TB
    subgraph 创建方式选择
        Q[需要传参] --> A[否: by viewModels]
        Q --> B[是: 需要注入]
        B --> C[否: 自定义 Factory]
        B --> D[是: Hilt ViewModel]
    end
方式写法适用场景
基础ViewModelProvider(this).get(MyVM::class.java)this 即 Owner,决定 ViewModel 归属哪个 Store、即作用域)理解原理、简单示例
委托(推荐)private val vm: MyVM by viewModels()无参、单 Activity/Fragment
自定义 Factoryby viewModels { MyFactory(args) }需要构造参数、无 Hilt
Hilt@HiltViewModel + @Inject constructor()有 Hilt 的现代项目

在 Activity 中:通常只用 by viewModels(),作用域为当前 Activity,该 Activity 独享。

在 Fragment 中:有两种常见用法——

  • by viewModels() → 作用域为当前 Fragment,该 Fragment 独享
  • by activityViewModels() → 作用域为宿主 Activity,多 Fragment 共享同一实例

2.3 Factory 的作用与为何要自定义

Factory 的作用:ViewModelProvider 从 Owner 的 ViewModelStore 里取 ViewModel 时,若没有缓存(第一次 get 这类 ViewModel),需要「创建」新实例。创建动作交给 ViewModelProvider.Factory:调用其 create(modelClass) 或 Lifecycle 2.5+ 的 create(modelClass, extras),由 Factory 决定如何 new 出这个 ViewModel 并返回。也就是说,Factory 负责「怎么构造 ViewModel」。

为什么要自定义:默认的 Factory 只能做无参构造,或通过框架提供的 CreationExtras(如 Application、SavedStateHandle)来构造。若你的 ViewModel 构造函数需要自己提供的参数(例如 userId、Repository、从 Intent 拿到的 id),默认 Factory 无法传入这些参数,就必须自定义 Factory:在创建 Factory 时带上这些参数,在 create() 里用它们调用 ViewModel 的带参构造函数。
用 Hilt 时,可由 Hilt 生成带依赖注入的 Factory,一般不需手写;只有「构造里需要运行时参数(如 id)」时才用 AssistedInject 或自定义 Factory 配合。

2.4 自定义 Factory 模板

class UserViewModel(private val userId: String, private val repo: UserRepository) : ViewModel()

class UserViewModelFactory(private val userId: String, private val repo: UserRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T = UserViewModel(userId, repo) as T
}
// Lifecycle 2.5+ 的 create(modelClass, extras) 会多一个 extras 参数,可从 extras 取 Application、SavedStateHandle 等,实现无状态 Factory。
// 使用
private val viewModel: UserViewModel by viewModels { UserViewModelFactory("user123", repo) }

2.5 ViewModel 作用域体系(完整)

ViewModel 的生命周期与清除时机完全由「它所属的 ViewModelStoreOwner」决定:选哪个 API 就是选哪个 Owner,同一 Owner 内相同 key 只会有一个 ViewModel 实例,不同 Owner 之间互不共享。

2.5.1 谁可以实现 ViewModelStoreOwner?

Owner 类型常见代表ViewModel 何时被清除
ActivityComponentActivityAppCompatActivityActivity 被 finish 且非配置变更时
FragmentFragment该 Fragment 被移除或其宿主 Activity 销毁时
Navigation 回栈条目NavBackStackEntry该条目从 BackStack 中弹出时

因此:作用域 = 你选择哪个 Owner。同一 Owner 内相同 key 对应同一个 ViewModel 实例(key 默认为 "androidx.lifecycle.ViewModelProvider.DefaultKey:" + ViewModel 类全限定名)。下面所有 API 都是在「选 Owner」。

作用域与 Owner 的关系(同一 Owner ⇒ 同一 ViewModelStore ⇒ 同一 key 对应同一 ViewModel):

flowchart TB
    subgraph Activity 作用域
        A[Activity] --> AS[ViewModelStore A]
        AS --> VM1[ViewModel 实例]
    end
    subgraph Fragment 作用域
        F[Fragment] --> FS[ViewModelStore F]
        FS --> VM2[ViewModel 实例]
    end
    subgraph 共享_同一 Activity
        A2[Fragment1 / Fragment2] --> AS2[requireActivity 的 Store]
        AS2 --> VM3[同一 ViewModel 实例]
    end

2.5.2 作用域 API 一览

使用场景Kotlin API(View 体系)得到的 Owner典型用途
Activity 内by viewModels()当前 Activity单屏数据、该 Activity 独享
Fragment 内by viewModels()当前 Fragment该 Fragment 独享
Fragment 需与 Activity 共享时by activityViewModels()宿主 Activity多 Fragment 共享同一 ViewModel
Fragment 需与父 Fragment 共享时by viewModels(ownerProducer = { requireParentFragment() })父 Fragment嵌套 Fragment 共享
与导航图绑定by navGraphViewModels(R.id.xxx)viewModels { getBackStackEntry(R.id.xxx) }该图在回栈中的 NavBackStackEntry同一导航图内共享,该 Entry 出栈后才清除

等价关系(便于理解):

// activityViewModels() 等价于
by viewModels(ownerProducer = { requireActivity() })

// navGraphViewModels(R.id.nav_graph) 等价于
by viewModels(ownerProducer = { findNavController().getBackStackEntry(R.id.nav_graph) })

注意getBackStackEntry(R.id.nav_graph) 传入的是导航图的 id,返回的是该图在回栈中的那条 NavBackStackEntry。只要该 Entry 还在回栈中(用户仍在该图内导航),多个 Fragment 以同一 Entry 为 owner,获取的就是同一个 ViewModel;当该 Entry 被 pop 出栈时,其 ViewModelStore 会被清除。

2.5.3 多 Fragment 共享示例

SharedViewModel 内需提供选中的条目(如 val selectedItem: LiveData<Item>)和设置方法(如 fun selectItem(item: Item)),两个 Fragment 用同一 owner 拿到同一实例即可。

// 列表:写
class ListFragment : Fragment() {
    private val shared: SharedViewModel by activityViewModels()
    fun onItemClick(item: Item) { shared.selectItem(item) }
}
// 详情:读(同一实例)
class DetailFragment : Fragment() {
    private val shared: SharedViewModel by activityViewModels()
    override fun onViewCreated(...) { shared.selectedItem.observe(viewLifecycleOwner) { showDetail(it) } }
}

2.6 AndroidViewModel 使用场景

当 ViewModel 必须使用 Context 时(如访问 SharedPreferences、系统服务、Resources),应使用 AndroidViewModel,它在构造函数中接收 Application,从而持有的是应用级 Context,生命周期与进程一致,不会因 Activity 重建而泄漏。

class SettingsViewModel(application: Application) : AndroidViewModel(application) {
    private val prefs = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
    // 或 application.resources、application.getSystemService(...) 等
}
  • 优先使用普通 ViewModel:若可通过 Repository、依赖注入获取数据,不必用 AndroidViewModel。
  • 不要在 ViewModel 中持有 Activity/Fragment/View 的引用;需要 Resources 或主题信息时,尽量通过接口或 UseCase 提供数据,而非在 ViewModel 中直接使用 Context。

3. ViewModel 实现原理

3.1 整体架构:谁持有谁

flowchart TB
    subgraph 组件关系
        Owner[ViewModelStoreOwner<br/>Activity/Fragment]
        Store[ViewModelStore<br/>HashMap]
        VM1[ViewModel 1]
        VM2[ViewModel 2]
        Owner --> Store
        Store --> VM1
        Store --> VM2
    end
  • ViewModelStoreOwner:接口,提供 getViewModelStore();Activity、Fragment、NavBackStackEntry 等实现了该接口。谁在什么时候调 getViewModelStore()?——ViewModelProvider.get() 内部需要先拿到 Store,就会调 owner.getViewModelStore(),再向 Store 要实例。
  • ViewModelStore:内部用 HashMap<String, ViewModel> 按 key 存储多个 ViewModel 实例。
  • ViewModelProvider:根据 Owner 与 Factory,从 Owner 的 Store 中「获取已有实例或创建新实例」。

3.2 创建与获取流程

sequenceDiagram
    participant UI as Activity/Fragment
    participant VP as ViewModelProvider
    participant Store as ViewModelStore
    participant Factory as Factory

    UI->>VP: get(MyViewModel::class)
    VP->>VP: key = "DefaultKey:包名.MyViewModel"
    VP->>Store: get(key)
    alt 已存在
        Store-->>VP: 已有实例
        VP-->>UI: 返回该实例
    else 不存在
        VP->>Factory: create(MyViewModel::class)
        Factory-->>VP: 新实例
        VP->>Store: put(key, viewModel)
        VP-->>UI: 返回新实例
    end

要点

  1. Key:默认是 "androidx.lifecycle.ViewModelProvider.DefaultKey:" + 类全限定名
  2. 先查再建:同一 Owner 内相同 key 只会有一个实例。配置变更后新 Activity 首次 get 时,getViewModelStore() 会从 getLastNonConfigurationInstance().viewModelStore 取回旧 Store,再 store.get(key) 得到复用后的旧 ViewModel 实例。
  3. Factory:只有「没有缓存」时才调用,由 Factory.create() 负责如何 new 出 ViewModel(含带参构造)。

3.3 by viewModels() 做了什么

by viewModels()Lazy 延迟初始化首次访问 viewModel 属性时才执行 ViewModelProvider(owner).get(VM::class),结果缓存在 Lazy 中,之后始终返回同一实例。

// 等价关系(首次访问时执行,未必在 onCreate)
private val viewModel: MyViewModel by viewModels()
// 等价于:首次访问时 ViewModelProvider(this).get(MyViewModel::class.java)

activityViewModels()viewModels()唯一区别是 Owner:前者以 requireActivity() 为 owner,因此多个 Fragment 获取的是同一 Activity 的 ViewModelStore 中的同一 ViewModel 实例。

3.4 屏幕旋转后 ViewModel 为何还在?(核心结论)

结论:ViewModel 不会随屏幕旋转丢失,是因为 ViewModelStore 在旧 Activity 销毁前被装进 NonConfigurationInstances,经 ActivityClientRecord 传给新 Activity,新 Activity 首次要 ViewModel 时从该对象里取回同一个 Store。全程内存引用传递,无序列化、无写盘

NonConfigurationInstances:配置变更时用于「保留对象」的容器,由系统在销毁前向 Activity 索取。为何强调 ComponentActivity?——Activity 基类的 onRetainNonConfigurationInstance() 默认返回 null,不会保留任何对象;真正把 mViewModelStore 装进 NonConfigurationInstances 并返回的是 ComponentActivity(AppCompatActivity 的父类),所以 ViewModel 的保留逻辑在 ComponentActivity 里。系统把返回值存到 ActivityClientRecord.lastNonConfigurationInstances


三阶段总览

总流程图

flowchart LR
    subgraph 一_保存
        A1[旧 Activity] --> A2[onRetain 返回 NCI] --> A3[存入 Record]
    end
    subgraph 二_传递
        B1[新 Activity] --> B2[attach 获得 NCI]
    end
    subgraph 三_取回
        C1[getViewModelStore] --> C2[取回 Store] --> C3[get 同一实例]
    end
    A3 --> B1
    B2 --> C1

三阶段简表

阶段时机关键方法 / 字段结果
一、保存旧 Activity 销毁前retainNonConfigurationInstances()onRetainNonConfigurationInstance() 返回 NonConfigurationInstances → 存入 ActivityClientRecord.lastNonConfigurationInstancesStore 引用从旧 Activity 到 Record
二、传递新 Activity attach 时activity.attach(..., record.lastNonConfigurationInstances) → Activity 赋给 mLastNonConfigurationInstances新 Activity 持有该引用,mViewModelStore 仍为 null
三、取回首次要 ViewModel 时getViewModelStore() 内从 getLastNonConfigurationInstance().viewModelStore 取回 → 赋给 mViewModelStore;无则 new ViewModelStore同一 Store → get(key) 得同一 ViewModel 实例

阶段一:保存

目的:把 ViewModelStore 的引用从旧 Activity 挪到 ActivityClientRecord,旧 Activity 可被回收,Record 稍后交给新 Activity。

步骤

  1. ActivityThread 销毁旧 Activity 前,调旧 Activity 的 retainNonConfigurationInstances()
  2. Activity 在 onRetainNonConfigurationInstance() 里 new NonConfigurationInstances,把 mViewModelStore 赋给其 viewModelStore,return(由 ComponentActivity 实现)。
  3. ActivityThread 将返回值存到 ActivityClientRecord.lastNonConfigurationInstances
  4. 引用链变为 Record → NonConfigurationInstances → ViewModelStore,旧 Activity 可回收。
sequenceDiagram
    participant AT as ActivityThread
    participant Old as 旧 Activity
    participant CA as ComponentActivity
    participant Record as ActivityClientRecord

    AT->>Old: retainNonConfigurationInstances()
    Old->>CA: onRetainNonConfigurationInstance()
    CA->>CA: nci.viewModelStore = mViewModelStore
    CA-->>Old: return NonConfigurationInstances
    Old-->>AT: return
    AT->>Record: record.lastNonConfigurationInstances = 该对象

阶段二:传递

目的:把 Record 里存的 NonConfigurationInstances 交给新 Activity。

步骤

  1. ActivityThread 用同一条 ActivityClientRecord 创建新 Activity,调 attach(..., record.lastNonConfigurationInstances, ...)
  2. Activity 在 attach() 里把该参数赋给 mLastNonConfigurationInstances
  3. 新 Activity 已持有含 viewModelStore 的 NonConfigurationInstances,此时尚未有人调 getViewModelStore(),mViewModelStore 仍为 null。
sequenceDiagram
    participant AT as ActivityThread
    participant Record as ActivityClientRecord
    participant New as 新 Activity

    AT->>New: 创建并 attach(..., record.lastNonConfigurationInstances)
    New->>New: mLastNonConfigurationInstances = 参数

阶段三:取回

目的:新 Activity 首次要 ViewModel 时,从 mLastNonConfigurationInstances 里取回旧 ViewModelStore,ViewModelProvider.get 得到同一 ViewModel 实例(懒加载,非 onCreate 自动执行)。

步骤

  1. 代码执行 ViewModelProvider(owner).get(MyViewModel::class),内部先调 owner.getViewModelStore()
  2. Activity 的 getViewModelStore():若 mViewModelStore 不为 null 直接返回;为 null 则从 getLastNonConfigurationInstance().viewModelStore 取,有则赋给 mViewModelStore 并返回,无则 new ViewModelStore()(由 ComponentActivity 实现)。
  3. 配置变更场景下会取到旧 Store,赋给 mViewModelStore;ViewModelProvider 再 store.get(key),得到同一 ViewModel 实例,恢复完成。
sequenceDiagram
    participant UI as Activity/Fragment
    participant VP as ViewModelProvider
    participant CA as ComponentActivity
    participant Store as ViewModelStore

    UI->>VP: get(MyViewModel::class)
    VP->>CA: getViewModelStore()
    alt mViewModelStore == null
        CA->>CA: mViewModelStore = getLastNonConfigurationInstance().viewModelStore
    end
    CA-->>VP: return mViewModelStore
    VP->>Store: get(key)
    Store-->>VP: 同一 ViewModel 实例
    VP-->>UI: 返回

三阶段一句话
① 保存 → ActivityThread 调 retainNonConfigurationInstances,Activity 返回含 Store 的 NonConfigurationInstances(ComponentActivity 实现),存到 Record.lastNonConfigurationInstances。
② 传递 → attach 时把 record.lastNonConfigurationInstances 传入,Activity 赋给 mLastNonConfigurationInstances。
③ 取回 → 首次 getViewModelStore() 时从 getLastNonConfigurationInstance().viewModelStore 取回赋给 mViewModelStore,ViewModelProvider.get(key) 得同一实例。

数据流:同一 ViewModelStore 引用沿「retainNonConfigurationInstances → ActivityClientRecord → attach → mLastNonConfigurationInstances → getViewModelStore() → mViewModelStore」传递,无序列化、无写盘。

3.5 ViewModel 何时被清除

Activity 在 onDestroy() 时有一个标志位 isChangingConfigurations:若为 true 表示本次销毁是因为配置变更(旋转、语言等),系统不会调用 ViewModelStore.clear();若为 false 表示用户真正退出(返回键、finish),才会 clear 并触发 onCleared()。

flowchart TB
    A[Activity.onDestroy] --> B[是否配置变更]
    B -->|是| C[不调用 clear<br/>ViewModel 保留]
    B -->|否| D[ViewModelStore.clear]
    D --> E[逐个 ViewModel.onCleared]
    E --> F[HashMap 清空]
场景ViewModel 是否保留是否调用 onCleared()
配置变更(旋转、语言等)保留并交给新 Activity
正常退出(返回键、finish)不保留
进程被系统杀死随进程消失

onCleared() 中应完成的清理:协程会随 viewModelScope 的 cancel 自动取消;此外应主动取消 Rx 的 Disposable、停止定时器、移除监听器等,避免泄漏和无效回调。

3.6 viewModelScope 原理简述

viewModelScope 是 ViewModel 的一个扩展属性,类型为 CoroutineScope。其行为可以概括为:

  • 绑定时机:ViewModel 被创建时,会有一个与该 ViewModel 绑定的 CoroutineScope。
  • 取消时机:当 ViewModelStore.clear() 被调用、该 ViewModel 被清理时,会先触发 onCleared(),同时与该 ViewModel 绑定的 Scope 会被 cancel,其下所有协程随之取消。
  • 实现方式:内部通过 ViewModel 的 Closeable 等机制,在 ViewModel 进入 cleared 状态时取消 Scope,避免 ViewModel 已失效后协程仍执行而造成泄漏或访问已销毁的 UI。

与清除的先后关系

flowchart TB
    A[Activity.onDestroy<br/>非配置变更] --> B[ViewModelStore.clear]
    B --> C[每个 ViewModel.onCleared]
    C --> D[viewModelScope.cancel]
    D --> E[该 Scope 下协程全部取消]

因此:在 ViewModel 中启动协程时,务必使用 viewModelScope.launch { ... },不要使用 GlobalScope 或未与 ViewModel 绑定的 Scope,这样在配置变更时不会泄漏,在 Activity/Fragment 真正退出时协程会自动取消。

3.7 ViewModelProvider 的 key 与 Factory 进阶

key 的生成规则

ViewModelProvider.get(MyViewModel::class) 内部会生成一个 key,默认是:

"androidx.lifecycle.ViewModelProvider.DefaultKey:" + modelClass.canonicalName

因此同一 Owner 内,同一 ViewModel 类对应同一个默认 key,从而对应同一个 ViewModel 实例。若在同一 Owner 下需要同一类的多个实例,可使用带 key 的重载:

ViewModelProvider(owner).get("customKey", MyViewModel::class.java)

此时 key 为 "customKey",与默认 key 不同,会得到另一个实例。

CreationExtras 与无状态 Factory(Lifecycle 2.5+)

从 Lifecycle 2.5.0 起,ViewModelProvider.Factorycreate 方法签名为:

fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T

CreationExtras 是一个只读的键值容器,在创建 ViewModel 时由框架传入,常用预定义 Key 包括:

Key含义
AndroidViewModelFactory.APPLICATION_KEYApplication 实例
NewInstanceFactory.VIEW_MODEL_KEY你传给 get(key, modelClass) 的 key(String)
SavedStateHandleSupport.DEFAULT_ARGS_KEY用于构造 SavedStateHandle 的 Bundle
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEYSavedStateRegistryOwner
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEYViewModelStoreOwner

这样 Factory 可以无状态:不必在构造 Factory 时传入 Application、Owner 等,而是在 create(modelClass, extras) 中从 extras 读取。例如用 APPLICATION_KEY 构造需 Application 的 ViewModel,或从 DEFAULT_ARGS_KEY 取 Bundle 构造 SavedStateHandle。一个 Factory 可根据 modelClassextras 支持多种 ViewModel 类型。


4. 内存泄漏与防护

4.1 什么是泄漏?为什么 ViewModel 容易牵涉进来?

泄漏:本应被回收的对象,因为还被别的存活对象引用,GC 收不走,占着内存不释放。

ViewModel 的设计是「配置变更时 Activity 销毁重建,ViewModel 不销毁」——ViewModel 比「当前这份 Activity 实例」活得更久。所以:

  • 若 ViewModel(或它内部的协程、观察者)还引用着已销毁的 Activity/View,Activity 就永远无法被 GC → 泄漏
  • 简单记:生命周期比 Activity 的对象(如随 Activity 销毁而释放的回调)可以持 Activity;生命周期比 Activity 的(ViewModel、静态变量、未取消的协程)不能持 Activity,否则 Activity 无法被回收。

4.2 三类泄漏来源

下面三类问题的共同前提是:ViewModel(或协程、观察者)比当前 Activity 实例活得久——配置变更时系统不会调用 ViewModelStore.clear(),ViewModel 会经 ActivityClientRecord 传给新 Activity 复用;只有用户真正退出(非配置变更的 onDestroy)才会 clear。所以一旦 ViewModel 等还引用着已销毁的 Activity,就会形成泄漏。

flowchart TB
    subgraph 泄漏来源
        A[持有 Activity 或 View 引用]
        B[未取消的协程或 Rx 订阅]
        C[观察者未随 Lifecycle 释放]
    end
    A --> D[ViewModel 比 Activity 存活更久]
    B --> D
    C --> D
错误做法引用链结果
ViewModel 持有 Activity / Fragment / ViewViewModelStore → ViewModel → Activity(已 onDestroy)Activity 被 ViewModel 拽着,GC 收不走 → Activity 泄漏
用 GlobalScope 或未取消的协程/Rx协程/订阅仍在跑 → 闭包引用 ViewModel 或 ActivityViewModel/Activity 本应回收,但被协程/订阅拽着 → 泄漏
观察者未移除,或 observer 强引用 ActivityViewModel → LiveData → observer → ActivityActivity 已销毁,但 observer 仍在列表里 → Activity 泄漏

4.3 正确写法与原因

场景正确做法错误做法与原因
Context只用 ApplicationAndroidViewModel(application)不要把 Activity/Fragment 通过构造或任何方式传进 ViewModel → 会强引用,配置变更时旧实例无法被 GC
协程只用 viewModelScope.launch { }GlobalScope 绑定进程,ViewModel/Activity 销毁时不会被 cancel;协程一直跑且闭包可能引用 ViewModel/Activity → 泄漏。viewModelScope 会在 ViewModel clear 时 cancel,协程与引用一起释放
RxJavaCompositeDisposable,在 onCleared()composite.clear()不清理则订阅一直存在,可能拽着 ViewModel/Activity → 泄漏
LiveDataobserve(lifecycleOwner, observer)系统在 DESTROYED 时自动移除观察者。避免在 observer 里强引用 Activity/View,否则间接形成 ViewModel → observer → Activity 链 → 泄漏

4.4 防护清单(速查)

  • 不持有 Activity / Fragment / View 引用;要 Context 只用 Application(如 AndroidViewModel
  • 协程统一用 viewModelScope;Rx 用 CompositeDisposable 并在 onCleared() 中清理
  • LiveData 用带 Lifecycle 的 observe(owner, ...);在 onCleared() 中释放定时器、监听器等
  • 不把 ViewModel 存到静态变量或单例里

4.5 反模式与常见错误

不要
在 ViewModel 中持有 Activity/Fragment/View 或非 Application 的 Context只用 Application Context(如 AndroidViewModel),或通过接口/UseCase 提供数据
把对话框显隐、EditText 焦点等纯 UI 状态都塞进 ViewModelViewModel 只保留屏幕级业务状态;纯 UI 状态用 remember/View 管理
在 ViewModel 里写满网络、数据库、复杂业务逻辑委托给 Repository/UseCase;ViewModel 只做「调依赖 + 映射 UI 状态 + 暴露流」
用 ViewModel 存大量数据或把 ViewModel 存静态/单例做全局共享持久化用 Room/DataStore;跨屏共享用 activityViewModels() / navGraphViewModels()
用 GlobalScope 或未在 onCleared() 中取消的 Rx/定时器/监听器协程用 viewModelScope;Rx 用 CompositeDisposable 并在 onCleared() 中 clear()

5. ViewModel 与 onSaveInstanceState

5.1 职责划分

维度ViewModelonSaveInstanceState
应对的问题配置变更(旋转、语言等)进程被杀死后恢复
数据存在哪内存,对象引用Bundle,可被系统持久化到磁盘
数据量与类型受内存限制,任意对象不宜过大(约 <1MB),基本类型/Parcelable/Serializable
恢复时机首次 getViewModelStore() 时取回旧 Store,再 get(key) 得同一 ViewModel 实例需在 onCreate(savedInstanceState) 中手动读取
flowchart TB
    subgraph ViewModel
        V1[配置变更] --> V2[Store 引用传递]
        V2 --> V3[无序列化]
    end
    subgraph Bundle
        S1[进程被杀] --> S2[序列化到磁盘]
        S2 --> S3[重建时反序列化]
    end

5.2 现代做法:ViewModel + SavedStateHandle

既要「配置变更不丢」又要「进程死后能恢复」时,用 SavedStateHandle 存少量 key(如 id、搜索词),大块数据仍放 ViewModel。用法:在 ViewModel 构造参数中声明 SavedStateHandle,用 get/set、getStateFlow 读写;支持类型与 Bundle 一致,不宜过大;用户从最近任务划掉、强制停止、重启或 finish 后冷启动则无法恢复。

class UserProfileViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    var userId: String
        get() = savedStateHandle.get<String>("user_id") ?: ""
        set(v) { savedStateHandle.set("user_id", v) }
    private val _user = MutableStateFlow<User?>(null)
    val user = _user.asStateFlow()
    fun loadUser() { viewModelScope.launch { _user.value = repo.getUser(userId) } }
}

5.3 选择策略

(此处「恢复」指进程被系统杀死后重新启动时能否获取上次的状态,非指长期持久化到磁盘。)

flowchart TB
    Q[是否需要进程死亡后恢复] --> VM[否则仅 ViewModel]
    Q --> Q2[数据量小且可序列化]
    Q2 --> Bundle[是: SavedStateHandle 等]
    Q2 --> SS[否: ViewModel 加 SavedStateHandle 存 key]

「ViewModel + SavedStateHandle 存 key」指:在 SavedStateHandle 里只存 id、搜索词等少量 key,进程死后用 key 恢复;大块数据仍放 ViewModel 或按 key 从 Repository 拉取。


6. SavedStateHandle 详解

SavedStateHandle 的用法与限制如下。ViewModel 只解决配置变更下的数据保留;进程被系统杀死后,ViewModel 会随进程消失。若要在进程重建后恢复少量 UI 状态(如用户 ID、搜索关键字、列表滚动位置 key),需使用 SavedStateHandle

6.1 是什么、存在哪

  • SavedStateHandle:一个键值对容器,由系统在 Activity/Fragment 的 SavedStateRegistry 背后支持。
  • 保存时机:在 onStop() 前后,SavedStateRegistry 会收集并写入需保存的状态到 Bundle(可能与任务栈一起被系统持久化)。
  • 恢复时机:Activity/Fragment 因进程重建而重新创建时,系统先恢复 Bundle,再在创建 ViewModel 时将基于该 Bundle 构造的 SavedStateHandle 通过构造函数或 Factory 传入。使用默认的 by viewModels() 时框架会自动注入 SavedStateHandle,在 ViewModel 构造参数中声明即可,无需手写 Factory。

因此:SavedStateHandle 里放的数据,既能在配置变更后存在(因为 ViewModel 本身还在),也能在进程死亡后被系统恢复(因为写进了 SavedStateRegistry 的 Bundle)。

SavedStateHandle 的写入与恢复流程(与 ViewModel 的「内存引用」恢复是两条独立路径):

flowchart TB
    subgraph 写入_进程存活时
        W1[ViewModel 内 set] --> W2[SavedStateRegistry 收集]
        W2 --> W3[onStop 前后写入 Bundle]
        W3 --> W4[系统可能序列化到磁盘]
    end
    subgraph 恢复_进程死后重建
        R1[Activity 重建] --> R2[系统恢复 Bundle]
        R2 --> R3[构造 SavedStateHandle]
        R3 --> R4[get 获取进程死前的值]
    end
    W4 -->|进程被杀| R1

6.2 基本用法

在 ViewModel 构造参数中声明 SavedStateHandle 即可,默认 by viewModels() 会自动注入。下面示例为 get/set 与 getStateFlow 的用法:

class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    var searchQuery: String
        get() = savedStateHandle.get<String>("query") ?: ""
        set(v) { savedStateHandle.set("query", v) }
    val queryFlow = savedStateHandle.getStateFlow("query", "")  // 可观察
}
  • get/set:读写;getStateFlow(key, default):暴露为 Flow 便于 UI 收集。

6.3 支持的类型与限制

可保存的类型与 Bundle 一致:基本类型、String、Parcelable、Serializable、以及受支持的集合等。不宜存大对象或大量数据,总大小建议控制在约 1MB 以内,否则可能影响保存/恢复性能或被系统限制。

6.4 何时能恢复、何时无法恢复

能恢复的典型情况:进程仅因内存紧张被系统回收,用户从最近任务再次进入应用;或从其他应用返回时进程被重建。只要任务栈与 SavedState 被系统保留,就会恢复。

无法恢复的情况:下次启动为「冷启动」,系统不会提供上次的 SavedState 数据,例如:

  • 用户从最近任务列表划掉应用
  • 用户在设置中强制停止应用
  • 设备重启
  • 用户按返回键或调用 finish() 退出该 Activity 后,再重新启动应用

7. ViewModel 与 Compose / Navigation

7.1 Compose 中的 viewModel()

在 Compose 中,ViewModel 的作用域不能绑定到「某个 @Composable 函数」——因为 Composable 会随重组多次执行,若每次拿新的 ViewModel 会丢状态且难以管理。必须绑定到 ViewModelStoreOwner(如宿主 Activity、Fragment,或当前 Navigation 目的地的 NavBackStackEntry)。用法示例:

@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // 根据 uiState 渲染
}
  • viewModel() 使用的 owner 来自 LocalViewModelStoreOwner(在 NavHost 的 composable 中一般为当前目的地的 NavBackStackEntry,否则为宿主 Activity 或 Fragment)。

7.2 指定 Owner(Activity / 父 Fragment / 导航图)

需要与 Activity 或父 Fragment 共享 ViewModel 时,显式传入 viewModelStoreOwner

// 与 Activity 共享:在 Fragment 的 Composable 中可用 requireActivity();纯 Compose 中可用 LocalContext.current 转成 Activity(确保 owner 为 Activity 时)
// 与某导航图共享: val entry = navController.getBackStackEntry("graphId"); viewModel(entry)

7.3 Fragment + Navigation:navGraphViewModels

在 View 体系下,若希望 ViewModel 与某个导航图绑定(在该图对应的 NavBackStackEntry 仍在回栈内时,多个 Fragment 共享同一实例;该 Entry 出栈后清除):

import androidx.navigation.fragment.navGraphViewModels

class MyFragment : Fragment() {
    private val sharedViewModel: SharedViewModel by navGraphViewModels(R.id.nav_graph)
}

等价于 viewModels(ownerProducer = { findNavController().getBackStackEntry(R.id.nav_graph) })。使用 Hilt 时可用 hiltNavGraphViewModels(R.id.nav_graph),由 Hilt 生成可注入依赖的 ViewModelProvider.Factory。


8. ViewModel 与 Hilt

8.1 基本用法:@HiltViewModel 与 @Inject

使用 Hilt 时,ViewModel 由 Hilt 生成的 ViewModelProvider.Factory 创建,无需手写 Factory;构造参数(如 Repository、SavedStateHandle)可由 Hilt 注入:

@HiltViewModel
class UserViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    private val repo: UserRepository
) : ViewModel() { fun loadUser() { viewModelScope.launch { /* 用 repo */ } } }

Activity/Fragment 加 @AndroidEntryPoint,仍用 by viewModels() 获取。

Hilt 会为 ViewModel 生成 ViewModelProvider.Factory,并在内部使用 ViewModelProvider(owner, factory).get(),因此作用域仍由 owner(Activity、Fragment 或 NavBackStackEntry)决定。

8.2 ViewModel 内依赖的作用域

  • 注入到 ViewModel 的依赖(如 Repository)通常由 SingletonActivity 等 Hilt 组件提供,其生命周期由 Hilt 作用域决定,与 ViewModel 何时被清除无必然关系。
  • 若希望某依赖「仅在该 ViewModel 实例内有效」,可使用 Hilt 提供的 @ViewModelScoped;同一 ViewModel 类的不同实例会得到不同的 @ViewModelScoped 实例。

8.3 带运行时参数的 ViewModel(AssistedInject)

若 ViewModel 除 Hilt 可注入的类型外,还需要运行时参数(如从 Intent/Navigation 传来的 id),常见做法是:把 id 放进 SavedStateHandle(例如通过 Navigation 的 defaultArguments 或 Activity 的 intent.extras),ViewModel 构造里声明 SavedStateHandle 由 Hilt 注入,在构造函数内用 savedStateHandle.get<String>("id") 取 id,其余依赖照常由 Hilt 注入。若必须通过 Factory 传参,可用 @AssistedInjectAssistedFactory,详见 Hilt 官方文档「ViewModel with assisted state」。


9. ViewModel 状态暴露:LiveData / StateFlow / SharedFlow

9.1 三者简要对比

特性LiveDataStateFlowSharedFlow
生命周期感知是,观察者在 Lifecycle DESTROYED 时自动移除,不会泄漏否,需配合 repeatOnLifecycle 等
必有初始值是(value)
重放仅最新值仅最新值可配置 replay
场景传统 View、DataBindingCompose、现代 Kotlin 优先一次性事件、多订阅者

9.2 在 ViewModel 中如何选

  • Compose 或 Kotlin 优先:推荐用 StateFlow 暴露 UI 状态,在 UI 层用 collectAsStateWithLifecycle()repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect { } } 收集,避免在 STOPPED/DESTROYED 时仍收集造成浪费或问题。
  • 传统 View + XMLLiveData 仍适用,observe(lifecycleOwner, observer) 即可,由系统在 DESTROYED 时移除观察者。
  • 一次性事件(Toast、导航、Snackbar):用 SharedFlow(replay=0) 或 Channel 在 ViewModel 中发送,在 UI 层按需消费,避免配置变更后重复触发。

9.3 推荐模式:单一 StateFlow + 事件流

状态StateFlow(有初始值、始终有当前值);一次性事件(Toast、导航、Snackbar)用 SharedFlow(replay = 0),这样配置变更后不会重放旧事件,避免重复触发。

private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>(replay = 0)

10. ViewModel 测试

10.1 为何好测

ViewModel 不依赖 Android 框架的 Context、View,只依赖 Repository、UseCase 等接口,便于在 JVM 单元测试中 mock 依赖、验证状态与副作用。

10.2 依赖与规则

  • androidx.arch.core:core-testing:提供 InstantTaskExecutorRule,让 LiveData 的 postValue 等在同一线程同步执行,便于断言。
  • kotlinx-coroutines-test:提供 TestScoperunTestUnconfinedTestDispatcher 等,在测试中控制协程调度,避免真实 delay/Dispatcher.Main。若测试中会触发 Main 协程,需用 TestDispatcher 替换 Main(如 Dispatchers.setMain(UnconfinedTestDispatcher()))或使用提供 MainDispatcherRule 的 test 库。
@get:Rule val instantExecutorRule = InstantTaskExecutorRule()  // LiveData 同步
// MainDispatcherRule 需自行实现或使用 test 库提供的 Main 替换,以便 viewModelScope 等在主线程的测试中可控

10.3 测试思路

  1. 用 MockK 或 Mockito 构造 FakeRepository / Mock,注入 ViewModel。
  2. 调用 ViewModel 的方法(如 loadUser、onRefresh)。
  3. StateFlow:在 runTest 中 collect 或取 value,断言状态变化。
  4. LiveData:用 InstantTaskExecutorRule 后直接取 value 或 observe 一次断言。
  5. 验证 viewModelScope 内协程:在 TestDispatcher 下 runTest,确认在 cancel 或 advanceUntilIdle 后无多余发射或回调。

11. 总结与面试要点

11.1 核心价值

  • 配置变更不丢数据:ViewModelStore 经 retainNonConfigurationInstances → Record → attach → getViewModelStore() 传给新 Activity。
  • 生命周期边界清晰:仅在 Owner 因非配置变更而被销毁时清除,便于在 onCleared() 中做资源释放。
  • 便于架构分层:UI 只观察状态和转发事件,业务与数据在 ViewModel/Repository。
  • 易测试:不依赖 Android 组件的 ViewModel 可单独做单元测试。

11.2 最佳实践简表

建议做建议不做
by viewModels() / activityViewModels()在 ViewModel 中持有 View/Activity 引用
viewModelScope 启动协程用 GlobalScope 或未管理的 Disposable
用 Repository + 状态容器(StateFlow/LiveData)在 ViewModel 中堆砌大量裸数据
需要进程恢复时用 SavedStateHandle忽略 onCleared 的资源释放
为 ViewModel 写单元测试把 ViewModel 当全局单例用

11.3 常见面试题速答

  1. 旋转屏幕后 ViewModel 为什么还在?
    配置变更销毁前,ActivityThread 调 retainNonConfigurationInstances(),Activity 在 onRetainNonConfigurationInstance() 中返回含 Store 的 NonConfigurationInstances(ComponentActivity 实现),ActivityThread 存入 ActivityClientRecord.lastNonConfigurationInstances;新 Activity 在 attach() 时收到该对象赋给 mLastNonConfigurationInstances,首次 getViewModelStore() 时从 getLastNonConfigurationInstance().viewModelStore 取回并赋给 mViewModelStore,ViewModel 实例一直在内存里。

  2. ViewModel 和 LiveData 的区别?
    ViewModel 负责持有和管理 UI 相关状态;LiveData 是一种可观察、生命周期感知的容器,常用来把 ViewModel 里的状态暴露给 UI。两者配合使用,职责不同。

  3. 如何避免 ViewModel 导致的内存泄漏?
    只用 Application Context;协程用 viewModelScope;Rx 在 onCleared() 中 dispose;不在 ViewModel 中持有 Activity/Fragment/View;LiveData 用带 Lifecycle 的 observe。

  4. ViewModel 和 onSaveInstanceState 分别解决什么问题?
    ViewModel 解决配置变更导致的数据丢失(内存引用传递);onSaveInstanceState 解决进程被杀死后的恢复(Bundle 序列化)。两者可结合 SavedStateHandle 一起用。

  5. ViewModel 有哪些作用域?
    Activity 内(viewModels())、Fragment 内(viewModels())、与 Activity 共享(activityViewModels())、与父 Fragment 共享(ownerProducer = { requireParentFragment() })、与导航图绑定(navGraphViewModels(R.id.xxx))。作用域由 ViewModelStoreOwner 决定,Owner 因非配置变更被销毁时 ViewModel 被清除。

  6. SavedStateHandle 和 ViewModel 的区别?
    ViewModel 解决配置变更时内存中数据保留;SavedStateHandle 在 ViewModel 内提供键值对,由 SavedStateRegistry 支持,可在进程死亡后随 Activity 恢复。适合存少量关键状态(如 id、搜索词),大块数据仍放 ViewModel 或 Repository。

  7. Compose 里怎么用 ViewModel?
    使用 viewModel() 获取作用域为「当前 Composition 的 ViewModelStoreOwner」的 ViewModel;在 Navigation 中一般为当前目的地。需共享时传 viewModelStoreOwner 参数(如 requireActivity()、parentBackStackEntry)。

  8. 如何对 ViewModel 做单元测试?
    Mock Repository 等依赖并注入;使用 InstantTaskExecutorRule 让 LiveData 同步;使用 kotlinx-coroutines-test 的 runTest/TestDispatcher 控制协程;对 StateFlow 取 value 或 collect 断言,对 LiveData 观察一次或取 value 断言。