使用viewModelScope启动协程失败的问题分析

911 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

前言

事情是这样的,在一次Fragment页面的viewModel里面用协程的过程中发现协程代码不执行了,what? 难道使用viewModelScope开启协程有坑?理论上不应该啊,然后就有了以下这些过程。

72afd3dfd5044d727771627731041c28.jpeg

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也应该清除数据,不再去使用了。

1320b9bee6d0643862c2311efa59d0a2.gif