当前位置: 首页 > news >正文

Android 关于activity-ktx的 by viewModels()踩坑记录与分析

LiveData 订阅问题分析与总结

问题描述

在 Android Activity 中,使用 viewModel.data.observe(this) { ... } 对 LiveData 进行订阅时,出现概率性订阅失败的现象:代码确定已执行,但数据更新后 Observer 回调始终无法触发。问题在调换 initObserver()lifecycleScope.launch { vm.refreshData() } 两行代码的顺序后复现或消失。

1. 问题起因

问题的根本起因是一个多线程环境下的初始化竞态条件

  • 错误操作顺序:在 Activity 的 onCreate 方法中,先启动一个 IO 协程并在其中首次调用 vm.refreshData(),然后再在主线程调用 initObserver() 进行订阅。

private val vm: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {lifecycleScope.launch(Dispatchers.IO) {vm.update("1234567890")}vm.data.observe(this) {Toast.makeText(this, it, Toast.LENGTH_SHORT).show()}
}
  • 触发机制by viewModels() 是延迟属性委托(lazy),ViewModel 实例的创建发生在首次访问 vm 属性时。上述错误顺序导致了对 vm首次访问发生在错误的线程(IO线程)和错误的时间点(Activity初始化阶段)

2. 分析过程

2.1 初步分析与错误假设

  1. 表面现象:LiveData 有时能订阅到,有时不能,像是随机失败。
  2. 初步排查
    • 检查了 ViewModel 的实现(后备属性模式),确认实现正确。

    • 检查了生命周期状态,确认 Observer 已添加但为非活跃状态。

    • 出现问题时,获取data的订阅者列表为空白

  3. 错误假设:最初怀疑是数据更新时机与生命周期状态不匹配导致的、初始化途中抛了异常导致订阅代码没执行

2.2 决定性证据与深入排查

  1. 哈希码比对:在订阅和设置值的地方打印 LiveDataViewModelSystem.identityHashCode(),发现两者哈希码均不相同,证明操作的是完全不同的对象实例。
  2. 构造函数日志:在 ViewModel 的 init 块中添加日志,发现构造函数被调用了两次,铁证如山地表明创建了两个实例。
  3. 内存分析(Heap Dump):使用 Android Profiler 捕获堆转储,发现内存中确实存在两个 ViewModel 实例:
    • 实例A:[正确用法] 先observe(this)再在IO中执行刷新数据,被正式存储在 ActivityViewModelStore 中,UI 线程订阅于此。
      在这里插入图片描述

    • 实例B:[错误用法] 先在IO中执行刷新数据再observe(this),被 by viewModels() 委托的内部缓存 (ViewModelLazy.cached) 持有。
      在这里插入图片描述

2.3 根本原因定位

基于所有证据,推断出根本原因:
IO线程:执行了首次访问,创建了实例B,并将其缓存在了 ViewModelLazy 的 cached 字段中。

主线程:几乎同时也执行了首次访问。由于IO线程的缓存操作可能尚未对主线程可见,或者框架检测到某种状态不一致,主线程的 ViewModelProvider 绕过了缓存,选择去 ViewModelStore 中重新创建并存储了一个新实例(实例A)。

// 顺序二(出问题的顺序)
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)lifecycleScope.launch(Dispatchers.IO) { // [线程: IO]// 在IO线程和Activity初始化阶段,框架状态可能不稳定。// 创建了实例B,并可能被缓存。vm.refreshData() // 操作的是实例B}initObserver() { // [线程: Main]// 再次首次访问 `vm`?由于竞态条件,主线程的初始化流程// 可能未感知到IO线程创建的实例,于是创建了实例A。vm.data.observe(this) { ... } // 订阅的是实例A}
}

结论:由于 by viewModels() 委托在 Dispatchers.IO 上首次初始化时,与主线程的初始化流程发生竞态条件,导致框架内部状态不一致,最终错误地创建了两个实例。订阅和更新操作分别应用在了不同的实例上。

3. 结论与解决方案

3.1 结论

  • 问题性质:这不是 LiveData 或 ViewModel 的 bug,而是by viewModels() 委托机制理解不足而引发的使用方式错误
  • 核心原因:在 Activity 生命周期的初始化阶段 (onCreate),在后台线程触发 ViewModel 的首次初始化,导致框架内部产生竞态条件,创建了多个实例。
  • 最终表现:UI 订阅了一个实例,数据更新发生在另一个实例,造成“订阅失效”的假象。

3.2 解决方案与最佳实践

解决方案:调整代码顺序,确保 by viewModels() 的首次初始化发生在主线程。

// 正确顺序
override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 1. 先在主线程完成订阅(这会触发ViewModel的安全初始化)initObserver()// 2. 然后再启动异步任务更新数据loadData()
}private fun initObserver() {vm.data.observe(this) { ... } // 首次访问 `vm`,在主线程创建实例
}private fun loadData() {lifecycleScope.launch(Dispatchers.IO) {vm.refreshData() // 后续访问,直接使用已创建的实例}
}

最佳实践

  1. 唯一初始化点:将在 onCreate/onViewCreated在主线程进行订阅作为初始化 ViewModel 的标准方式。
  2. 避免跨线程初始化:绝对避免在后台线程或异步回调中首次访问 by viewModels() 委托的属性。
  3. 依赖传递:如果其他类需要 ViewModel,应通过参数传递已获取到的实例,而不是重新获取。

4. 经验教训

  1. 理解委托机制:深刻理解 by viewModels() 是延迟执行的,其初始化时机至关重要。
    *:如果其他类需要 ViewModel,应通过参数传递已获取到的实例,而不是重新获取。

4. 经验教训

  1. 理解委托机制:深刻理解 by viewModels() 是延迟执行的,其初始化时机至关重要。
  2. 线程安全意识:Android 组件的初始化(尤其是生命周期相关的组件)通常对线程敏感,应严格遵守在主线程操作的原则,使用by viewModels()时,需要保证先在主线程使用一次viewmodel,使他创建好实例!!!
http://www.dtcms.com/a/344568.html

相关文章:

  • 龙蜥Confidential MaaS解决方案如何破解MaaS “黑盒”困局|《AI 进化论》第三期
  • MATLAB:编程入门、多维可视化、时间序列/图像/地图/遥感/点云数据处理及生态模型构建
  • 软件设计师——计算机网络学习笔记
  • 汽车主机厂为何开始押注平台化视觉?
  • 微服务的编程测评系统14-C端题目列表功能-个人中心
  • uniapp使用map打包app后自定义气泡不显示解决方法customCallout
  • Java设计模式--工厂模式:对象创建的魔法工坊
  • GDSFactory环境配置(PyCharm+Git+KLayout)
  • C/C++三方库移植到HarmonyOS平台详细教程(补充版so库和头文件形式)
  • 如何使用navicat连接容器中的mysql数据库
  • 报表工具DevExpress .NET Reports v25.1新版本亮点:AI驱动的扩展
  • Tensorflow、Keras与Python版本兼容性全解析
  • xml中resultMap 的用法,数据库 JSON 字符串 → Java List/对象
  • Build a Webhook for a Chatbot Using Python
  • Python处理JSON数据的最佳实践:从基础到进阶的实用指南
  • 深入理解深度学习中的“Batch”
  • SSM框架基础知识-Spring-Spring整合MyBatis
  • 数据安全——39页解读数字化转型大数据安全基础培训方案【附全文阅读】
  • [react] js容易混淆的两种导出方式2025-08-22
  • 6020角度双环控制一种用于电机控制的策略
  • Numpy模块下的ndarray介绍
  • vscode 插件 远程服务器无法下载
  • Axure下载安装教程(附安装包)Axure RP 11 超详细下载安装教程
  • AI多模态分析框架下的黄金下跌波动:鲍威尔讲话前的政策信号与量化因子共振
  • Mongodb操作指南
  • kafka的rebalance机制是什么
  • 赛思电子工业级晶振,工业控制的隐形“智”动力
  • Linux服务器定时监测服务脚本
  • det_cam_visualizer.py 函数逐行解读记录
  • (纯新手教学)计算机视觉(opencv)实战八——四种边缘检测详解:Sobel、Scharr、Laplacian、Canny