android ViewModel liveData无法监听之多线程下activityViewModels不安全

发布于:2025-05-09 ⋅ 阅读:(18) ⋅ 点赞:(0)

我们一般的,会遇到liveData无法监听到结果,可能存在主要2种可能:

  1. liveData没有正确注册;
  2. liveData连续多次设置值,中间的值,会被丢弃,但最后一次是能监听到的。

但是我们容易忽略一种case,检查你的多线程执行,你的viewModel可能被创建了多次
先说结论:

fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)
//bag: not safe
private val viewModel by unsafeLazy { ViewModelProvider(requireActivity())[MyViewModel::class.java] }
//bag: not safe too
private val viewModel: MyViewModel by activityViewModels()

如上2种都不能在多线程下保险。尤其是官方的by activityViewModels() 容易让你以为它是安全的。但事实上,你的viewModel仍然可能出现问题!

//fragment or activity的onCreateView函数。
//1. 这部分代码原来还隐藏到其他类中。
mFileMgr.loadFileList()

//......other....
//2. 初始化监听
viewModel.xxxLiveData.observe(this){ 
	//....
}


//FileMgr类
fun loadFileList() {
	lifecycleScope.launchOnThread { //fragment/activity的scope发起子线程
		val fileList = viewModel.suspendLoadFileList()
	    lifecycleScope.launch {
	       //do something....
	    }
	}
}

理解下代码初衷:
我想要异步读取文件列表。写了一个suspend函数LoadFileList在viewModel里面。然后在某个专门处理文件的类里面调用的。
最开始我怀疑我的监听哪里有问题,postValue/setValue存在问题等。
直到梳理代码简化成这样才发现是多线程创建viewModel的问题。

显然,代码是有问题的,先切了子线程,会触达viewModel,同时主线程下面的viewModel.xxxLiveData也会触达。
这样就形成了多线程竞争,同时初始化了2个viewModel,进而导致你监听的liveData已经被别的ViewModel取代。

lazy LazyThreadSafetyMode.NONE可能你能怀疑到,它是一个线程不安全的。
但是,官方库by activityViewModels() 也会出问题,你是没有想到的。

改进

方案1: 使用标准lazy,而不是LazyThreadSafetyMode.NONE

private val viewModel by lazy { ViewModelProvider(requireActivity())[MyViewModel::class.java] }

方案2: lateinit var 在onCreate里面去新建它。稍微比by的方式麻烦,不够简洁。
但是优点很多:
编译后字节码较少:(相较于by懒加载会被创建一些lazy对象,少了不少。)
天然想到最先初始化:类似传统java代码,编码的时候,你肯定想到的在onCreate最前面去创建它,确保了一定初始化和唯一性。
我这里的例子就是我FileMgr类的执行早于主类中触达viewModel 的时机了。导致了问题。

private lateinit var viewModel : MyViewModel

override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
    viewModel = ViewModelProvider(requireActivity())[MyViewModel::class.java]
}

方案3: 不要让子线程更早可能触达viewModel。因为是by懒加载模式,那么,让主线程更早的接触viewModel变量即可。

//先直接主线程触达viewModel
viewModel.xxxLiveData.observe(this){ //....}
mFileMgr.loadFileList()

方案4: 改成viewModel.viewModelScope。这样并不是说因为是scope的原因,是因为触发懒加载。因为函数的调用是主线程,触达viewModel就在主线程了,避免了竞争。

fun loadFileList() {
	viewModel.viewModelScope.launchOnThread { //fragment/activity的scope发起子线程
		val fileList = viewModel.suspendLoadFileList()
	    lifecycleScope.launch {
	       //do something....
	    }
	}
}

总结

对于项目中存在的unsafeLazy的,不仅仅是针对viewModel,
都建议检查你是否有可能多线程竞争问题;
如果,多创建一次对象没啥影响的就无所谓就继续使用。有任何可能,就改成lazy。

对于viewModel的初始化,推荐方案1和方案2。不推荐官方的写法。
如果用官方写法,请自行把握viewModel的触达,确保最早在主线程中被创建。


网站公告

今日签到

点亮在社区的每一天
去签到