在 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()