在 Android 开发(Kotlin)中,以下是声明 ViewModel 的几种方式及其核心区别:
1. private lateinit var mViewModel: MViewModel
- 手动初始化 :需要显式调用
ViewModelProvider(this).get(MViewModel::class.java)初始化。 - 风险 :忘记初始化会导致
UninitializedPropertyAccessException崩溃。 - 不推荐:代码冗余且易出错。
2. private val viewModel: MViewModel by lazy {}
kotlin
private val viewModel by lazy {
ViewModelProvider(this).get(MViewModel::class.java)
}
- 延迟初始化:首次访问属性时自动初始化(线程安全)。
- 问题:无法感知生命周期(如 Fragment 重建可能导致泄漏)。
- 不推荐用于 ViewModel :
lazy不处理生命周期安全。
3. private val viewModel: MViewModel by viewModels()
-
标准委托 (需依赖
androidx.fragment:fragment-ktx)。 -
自动生命周期感知:
- 在配置变更(如旋转屏幕)后自动恢复同一实例。
- 当
Fragment/Activity销毁时自动清理作用域。
-
扩展性 :支持自定义
Factory(通过viewModels { factory })。 -
作用域:
Activity中声明 → 作用域为当前Activity。Fragment中声明 → 作用域为当前Fragment。
4. private val mViewModel by viewModels<MViewModel>()
csharp
// 等价于:
private val mViewModel: MViewModel by viewModels()
- 语法糖:通过泛型推断类型,省略显式类型声明(更简洁)。
- 其他行为与
by viewModels()完全相同。
⚠️ 关键区别总结:
| 方式 | 初始化时机 | 生命周期感知 | 代码安全性 | 推荐度 |
|---|---|---|---|---|
lateinit var |
手动初始化 | ❌ 无 | ⚠️ 易出错 | 不推荐 |
by lazy {...} |
首次访问时 | ❌ 无 | ⚠️ 重建风险 | 不推荐 |
by viewModels() / <T>() |
首次访问时 | ✅ 自动处理 | ✔️ 安全 | ★★★★★ |
✅ 最佳实践:
直接用:
csharp
private val mViewModel by viewModels<MViewModel>()
// 或
private val mViewModel: MViewModel by viewModels()
- 简洁安全,且完美适配 Android 生命周期机制。
那你可能要有大大的问号❓,为啥,为啥,为啥by lazy {} 方式对 ViewModel 存在生命周期问题
详解:为什么 by lazy {} 方式对 ViewModel 存在生命周期问题?
在 Fragment 中使用 by lazy 初始化 ViewModel 时:
kotlin
private val viewModel by lazy {
ViewModelProvider(this).get(MViewModel::class.java)
}
会导致以下两个核心问题:
🔥 问题 1:重建时创建新实例(数据丢失)
-
场景:当 Fragment 因配置变更(如屏幕旋转)被销毁重建时
-
发生过程:
-
原 Fragment 实例 A 被销毁
-
新 Fragment 实例 B 被创建
-
当 B 首次访问
viewModel时,lazy块会执行:kotlinViewModelProvider(this) // 此处的 "this" 是实例B -
创建了全新的 ViewModel 实例
-
-
严重后果 :
✅ 原 ViewModel 中的数据全部丢失 (违背 ViewModel 设计初衷)
❌ 无法维持配置变更时的状态一致性
🔋 问题 2:潜在的内存泄漏风险
-
场景:当 Fragment 被移除(如返回栈 pop)但未销毁时
-
发生过程:
-
假设父 Activity 仍在运行
-
第一次访问
viewModel时:kotlinViewModelProvider(this) // 绑定了当前Fragment实例 -
当该 Fragment 实例被移除(但未销毁)时:
❌ ViewModel 持有该 Fragment 的上下文引用
❌ 延迟初始化块形成的闭包也持有 Fragment 引用
-
-
严重后果 :
💥 Fragment 实例和其 ViewModel 无法被垃圾回收
📈 重复进入此界面会导致内存持续增长
✅ by viewModels() 的正确处理机制:
csharp
private val viewModel by viewModels<MViewModel>()
-
智能作用域绑定
通过
ViewModelLazy内部实现,确保始终关联到正确的ViewModelStoreOwner(自动处理重建后的关联) -
单例复用机制
从当前组件的
ViewModelStore获取 ViewModel:iniViewModelStore viewModelStore = owner.getViewModelStore(); ViewModel viewModel = viewModelStore.get(key); // 优先取缓存 if (viewModel == null) { viewModel = factory.create(); // 仅当不存在时创建 viewModelStore.put(key, viewModel); } -
自动生命周期清理
当 Fragment/Activity 最终销毁(非配置变更)时:
scss// 在 ComponentActivity 中 protected void onDestroy() { if (!isChangingConfigurations()) { // 非配置变更 getViewModelStore().clear(); // 自动清理 } }
🌰 直观对比(旋转屏幕场景):
| 初始化方式 | 第一次进入 | 旋转屏幕后 |
|---|---|---|
by lazy |
创建 VM 实例 A | 创建新实例 B(数据丢失) |
by viewModels() |
创建 VM 实例 A | 复用实例 A(数据保留) |
💡 根本原因总结:
by lazy 的本质缺陷是:
- 初始化逻辑与环境无关 - 每次触发都机械执行代码块
- 无关联上下文感知能力 - 无法区分「正常启动」和「配置变更重建」
- 无引用关系管理 - 不会自动绑定到生命周期的作用域
而 viewModels() 委托是专为 Android 架构设计的,通过内部关联 LifecycleOwner 和 ViewModelStore 实现了生命周期安全的 ViewModel 管理。
那有朋友有要问了:by lazy {...} lateinit var 不都是懒加载,为啥lateinit var不会出错
你敏锐地捕捉到了关键点!lateinit var 和 by lazy 都是懒加载 ,但它们在 ViewModel 使用中的风险性质不同。让我们通过具体场景解释为什么 lateinit var 方式错误更明显,而 by lazy 的风险更隐蔽但更危险:
⚠️ lateinit var 的显性风险
kotlin
private lateinit var mViewModel: MViewModel
问题表现:
-
编译期无报错 ,但运行时如果未初始化直接访问 → 100% 崩溃
scssmViewModel.getData() // 抛出 UninitializedPropertyAccessException -
错误明显易排查:崩溃堆栈明确指向未初始化的属性
为何不易出现生命周期问题:
因为它 根本不允许你延迟初始化到错误时机 - 要么在 onCreate() 中正确初始化,要么直接崩溃。开发者被迫在早期生命周期处理初始化。
☠️ by lazy {...} 的隐性风险
kotlin
private val viewModel by lazy {
ViewModelProvider(this).get(MViewModel::class.java)
}
问题表现:
-
编译运行一切正常,初次使用无异常
-
风险在特定场景下爆发:
- ✅ 正常启动 → 首次访问时初始化
- 💥 屏幕旋转重建 → 创建新实例(数据丢失)
- 💥 Fragment 在返回栈中留存 → 内存泄漏
为何风险更隐蔽危险:
-
闭包陷阱
lazy块捕获当前this(Fragment 实例):kotlin// 等价于: private val viewModel by lazy { val owner: Fragment = this@MyFragment // 捕获当前实例 ViewModelProvider(owner).get(MViewModel::class.java) }当 Fragment 被移除(如
FragmentManager.popBackStack()):- 此闭包仍持有 Fragment 引用
- ViewModel 实例持有闭包引用
→ GC 无法回收 Fragment
-
重建机制失效
ViewModelProvider(this)中的this永远指向 当前实例:场景 当前 this行为 首次启动 Fragment 实例 A 创建 ViewModel 实例 X 旋转屏幕重建 Fragment 实例 B 创建 新 实例 Y → 原 ViewModel 实例 X 被遗弃
🌰 真实案例对比
需求 :在 Fragment 的 onViewCreated() 中加载数据
kotlin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.loadData() // 在此首次使用 ViewModel
}
| 方式 | 首次启动 | 旋转屏幕后 | 内存泄漏风险 |
|---|---|---|---|
lateinit var |
需在 onCreate() 初始化 否则直接崩溃 |
正常恢复数据 | 无 |
by lazy |
自动初始化 | 创建新实例 旧数据丢失 | 存在 |
by viewModels() |
自动初始化 | 恢复原实例 | 无 |
✅ 根本区别总结
| 特性 | lateinit var |
by lazy |
by viewModels() |
|---|---|---|---|
| 初始化触发 | 必须手动调用 | 首次访问自动触发 | 首次访问自动触发 |
| 生命周期感知 | 无 | 无 | ✅ 感知重建/销毁 |
| ViewModel 实例管理 | 依赖手动代码正确实现 | 每次访问新建(重建时) | ✅ 通过 ViewModelStore 自动复用 |
| Fragment 引用持有 | 无 | ☠️ 通过闭包持有 | 无 |
| 问题显性程度 | ⚡ 立即崩溃(易发现) | 💣 隐蔽数据丢失/泄漏 | 无问题 |
💡 为什么 by lazy 如此危险
因为它在 所有场景下都能正常运行,唯独在关键生命周期节点(重建/返回栈)中出错。开发者测试时很难覆盖这些边界场景,直到线上出现用户数据丢失报告或内存泄漏崩溃后才暴露问题。
📌 经验法则:永远不要使用
by lazy或lateinit var初始化 ViewModel,只用by viewModels()