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 架构设计的,通过内部关联 LifecycleOwnerViewModelStore 实现了生命周期安全的 ViewModel 管理。

那有朋友有要问了:by lazy {...} lateinit var 不都是懒加载,为啥lateinit var不会出错

你敏锐地捕捉到了关键点!​lateinit varby 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 lazylateinit var 初始化 ViewModel,只用 by viewModels()

相关推荐
儿歌八万首2 小时前
Jetpack Compose 中 Kotlin 协程的使用
android·ui·kotlin·协程·compose
00后程序员张5 小时前
iOS WebView 调试实战 全流程排查接口异常 请求丢失与跨域问题
android·ios·小程序·https·uni-app·iphone·webview
魑魅魍魉95276 小时前
android 信息验证动画效果
android
给钱,谢谢!6 小时前
Flutter权限管理终极指南:实现优雅的Android 48小时授权策略
android·flutter
archko6 小时前
compose multiplatform 常用库
android
Mr_Xuhhh9 小时前
QT窗口(4)-浮动窗口
android·开发语言·网络·数据库·c++·qt
csdn_li_121210 小时前
cocosCreator2.4 Android 输入法遮挡
android
技术与健康11 小时前
【Android代码】绘本翻页时通过AI识别,自动通过手机/pad朗读绘本
android·人工智能·智能手机