ViewModel创建方式以及by lazy的问题。

在 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 因配置变更(如屏幕旋转)被销毁重建时

  • ​​发生过程​​:

    1. 原 Fragment 实例 A 被销毁

    2. 新 Fragment 实例 B 被创建

    3. 当 B 首次访问 viewModel 时,lazy 块会执行:

      kotlin 复制代码
      ViewModelProvider(this)  // 此处的 "this" 是实例B
    4. ​​创建了全新的 ViewModel 实例​​

  • ​​严重后果​ ​:

    ✅ 原 ViewModel 中的​​数据全部丢失​ ​(违背 ViewModel 设计初衷)

    ❌ 无法维持配置变更时的状态一致性


🔋 问题 2:潜在的内存泄漏风险

  • ​​场景​​:当 Fragment 被移除(如返回栈 pop)但未销毁时

  • ​​发生过程​​:

    1. 假设父 Activity 仍在运行

    2. 第一次访问 viewModel 时:

      kotlin 复制代码
      ViewModelProvider(this)  // 绑定了当前Fragment实例
    3. 当该 Fragment 实例被移除(但未销毁)时:

      ❌ ViewModel 持有该 Fragment 的上下文引用

      ❌ 延迟初始化块形成的闭包也持有 Fragment 引用

  • ​​严重后果​ ​:

    💥 Fragment 实例和其 ViewModel ​​无法被垃圾回收​ ​

    📈 重复进入此界面会导致​​内存持续增长​​


✅ by viewModels() 的正确处理机制:

csharp 复制代码
private val viewModel by viewModels<MViewModel>()
  1. ​​智能作用域绑定​ ​

    通过 ViewModelLazy 内部实现,确保始终关联到正确的 ViewModelStoreOwner(自动处理重建后的关联)

  2. ​​单例复用机制​ ​

    从当前组件的 ViewModelStore 获取 ViewModel:

    ini 复制代码
    ViewModelStore viewModelStore = owner.getViewModelStore();
    ViewModel viewModel = viewModelStore.get(key); // 优先取缓存
    if (viewModel == null) {
        viewModel = factory.create(); // 仅当不存在时创建
        viewModelStore.put(key, viewModel);
    }
  3. ​​自动生命周期清理​ ​

    当 Fragment/Activity 最终销毁(非配置变更)时:

    scss 复制代码
    // 在 ComponentActivity 中
    protected void onDestroy() {
        if (!isChangingConfigurations()) { // 非配置变更
            getViewModelStore().clear();   // 自动清理
        }
    }

🌰 直观对比(旋转屏幕场景):

​​初始化方式​​ 第一次进入 旋转屏幕后
by lazy 创建 VM 实例 A 创建新实例 B(数据丢失)
by viewModels() 创建 VM 实例 A ​​复用​​实例 A(数据保留)

💡 根本原因总结:

by lazy 的本质缺陷是:

  1. ​初始化逻辑与环境无关​ - 每次触发都机械执行代码块
  2. ​无关联上下文感知能力​ - 无法区分「正常启动」和「配置变更重建」
  3. ​无引用关系管理​ - 不会自动绑定到生命周期的作用域

而 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

问题表现:

  1. ​​编译期无报错​ ​,但运行时如果未初始化直接访问 → ​​100% 崩溃​​

    scss 复制代码
    mViewModel.getData() // 抛出 UninitializedPropertyAccessException
  2. ​​错误明显易排查​​:崩溃堆栈明确指向未初始化的属性

为何不易出现生命周期问题:

因为它 ​​根本不允许你延迟初始化到错误时机​ ​ - 要么在 onCreate() 中正确初始化,要么直接崩溃。开发者被迫在早期生命周期处理初始化。


☠️ ​​by lazy {...} 的隐性风险​​

kotlin 复制代码
private val viewModel by lazy { 
    ViewModelProvider(this).get(MViewModel::class.java) 
}

问题表现:

  1. ​​编译运行一切正常​​,初次使用无异常

  2. ​​风险在特定场景下爆发​​:

    • ✅ 正常启动 → 首次访问时初始化
    • 💥 屏幕旋转重建 → ​创建新实例​(数据丢失)
    • 💥 Fragment 在返回栈中留存 → ​内存泄漏​

为何风险更隐蔽危险:

  1. ​​闭包陷阱​ ​
    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​
  2. ​​重建机制失效​ ​
    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()​​

相关推荐
汪海游龙3 小时前
流式回复结尾的那一下闪:逐帧定位 Compose 聊天列表的三处布局跳动
android·性能优化·kotlin
2501_915909066 小时前
Ipa Guard 防护上架,混淆、重签名和回归测试怎么做
android·ios·小程序·https·uni-app·iphone·webview
码点6 小时前
Android ANR 全解析 与 traces 分析实战
android
敲代码的瓦龙7 小时前
Jetpack?lifecycle!!!
android·kotlin
苦瓜花7 小时前
Hilt用使用及原理
android
深度学习04078 小时前
MySQL InnoDB Cluster 部署完整文档
android·mysql·adb
邪修king9 小时前
Re:Linux系统篇(三十七):进程间通信Chapter4:System V 共享内存深度解析:为什么它是最快的 IPC?
android·linux·运维
用户337922545689 小时前
Kiora SDK:在 Android 上组织端侧模型、工具与 A2A 协作
android
洞见前行9 小时前
你在 XML 时代学的渲染优化,进了 Compose 大多不再是重点——Recomposition 才是新的战场
android
ii_best10 小时前
按键精灵手机助手 v5.2.1 实测:inputText 逐字延时 + 调试面板多行复制,安卓脚本开发的两个输入痛点一次解决
android·开发语言·智能手机·自动化·ai编程·按键精灵