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

相关推荐
小趴菜822714 分钟前
安卓接入Kwai广告源
android·kotlin
2501_9160137435 分钟前
iOS 混淆与 App Store 审核兼容性 避免被拒的策略与实战流程(iOS 混淆、ipa 加固、上架合规)
android·ios·小程序·https·uni-app·iphone·webview
程序员江同学2 小时前
Kotlin 技术月报 | 2025 年 9 月
android·kotlin
码农的小菜园2 小时前
探究ContentProvider(一)
android
时光少年4 小时前
Compose AnnotatedString实现Html样式解析
android·前端
hnlgzb5 小时前
安卓中,kotlin如何写app界面?
android·开发语言·kotlin
jzlhll1235 小时前
deepseek kotlin flow快生产者和慢消费者解决策略
android·kotlin
火柴就是我5 小时前
Android 事件分发之动态的决定某个View来处理事件
android
一直向钱5 小时前
FileProvider 配置必须针对 Android 7.0+(API 24+)做兼容
android
zh_xuan5 小时前
Android 消息循环机制
android