android ViewModel liveData无法监听之多线程下activityViewModels不安全
我们一般的,会遇到liveData无法监听到结果,可能存在主要2种可能:
- liveData没有正确注册;
- 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 : MyViewModeloverride 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的触达,确保最早在主线程中被创建。