持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
前言
事情是这样的,在一次Fragment页面的viewModel里面用协程的过程中发现协程代码不执行了,what? 难道使用viewModelScope开启协程有坑?理论上不应该啊,然后就有了以下这些过程。
viewModelScope源码分析
查看viewModelScope源码发现内部是用了一个HashMap存储CoroutineScope 第一次调用会先创建好协程对象后保存到HashMap,也就是说协程对象后面是复用的
val ViewModel.viewModelScope: CoroutineScope
get() {
//从ViewModel类的HashMap获取CoroutineScope
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
//第一次调用后会保持到HashMap中
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
//带关闭接口的协程对象
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
我们再看看这个setTagIfAbsent方法干了哪些事情?
<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
//如果ViewModel被标记清除了 那么会直接关闭协程
closeWithRuntimeException(result);
}
return result;
}
从上面可以看出,前面都是保存到HashMap的代码,但是下面有一个if判断,这个是关键,如果mCleared值为真则,则取消协程。这个mCleared是什么?
mCleared变量是ViewModel类的一个状态值,看过ViewModel创建过程的都知道,ViewModel通过页面的ViewModelStoreOwner感知onDestroy生命周期,当页面不在使用时,ViewModel的mCleared变量会被赋值为True,那么答案出来了,如果是协程没有启动,那么很可能就是ViewModel的状态值标记为清除,页面被销毁了。
问题解决
从上面的结论可以看出,原来是我使用的Fragment页面被销毁了然后导致协程调用失效,再检查了一下发现我使用的是ViewPager2+Fragment。
ViewPager2大家知道,默认超过三个Fragemt页面时会销毁第一个Fragmnt页面,然后因为我是在声明变量的时候直接创建的ViewModel对象,当Fragment销毁后重新创建回来这个时候得ViewModel还是使用的上一次创建的对象,也就是那个被标记为清除状态的ViewModel,所以解决的办法就是Fragment重新创建的时候再创建一次viewModel对象就好了
修改前
//修改前:
val viewModel: PurifyStepViewModel by viewModels()
修改后
lateinit var viewModel: PurifyStepViewModel
//页面重新创建后调用
override fun initViewModel(): BaseViewModel? {
viewModel = ViewModelProvider(this).get(PurifyStepViewModel::class.java)
return viewModel
}
结论
回到开口那个疑问,当然不是viewModelScope有坑了,只是我们没有规范的使用ViewModel而已,想想viewModelScope这个设计也是很合理的,如果页面销毁后,理应对应的ViewModel也应该清除数据,不再去使用了。