关于Kotlin协程取消的问题

时间:2019-11-28 06:44:37

标签: android kotlin coroutine

我的应用中有类似以下的代码

class MyFragment : Fragment(), CoroutineScope by MainScope() {

    override fun onDestroy() {
        cancel()
        super.onDestroy()
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        doSomething()
    }

    private fun doSomething() = launch {
        val data = withContext(Dispathers.IO) {
            getData()
        }

        val pref = context!!.getSharedPreferences("mypref", MODE_PRIVATE)
        pref.edit().putBoolean("done", true).apply()
    }
}

在生产环境中,访问NPEs时,我在doSomething(中得到了许多context

我的假设是,在coroutine中调用cancel()后,onDestroy()被取消了,因此我不必费心检查context是否为空值。但是看起来continues甚至在调用cancel()之后仍要执行。我认为,如果在完成cancel()之后并在恢复withContext之前调用coroutines,就会发生这种情况。

所以我用以下内容替换了doSomething()

    private fun doSomething() = launch {
        val data = withContext(Dispathers.IO) {
            getData()
        }

        if (isActive) {
            val pref = context!!.getSharedPreferences("mypref", MODE_PRIVATE)
            pref.edit().putBoolean("done", true).apply()
        }
    }

这可以解决崩溃问题。

但是,这是预期的行为还是我做错了什么? Kotlin的文档对此不太清楚。在线上的大多数示例都类似于我的原始代码。

3 个答案:

答案 0 :(得分:4)

您的代码假定withContext()将在返回时停止执行,如果取消了作用域,但实际上并没有执行,直到kotlin协程的1.3.0版本。这是GitHub issue。我猜您正在使用该库的早期版本。

我还建议您使用LifecycleScope而不是自定义范围。它是lifecycle-runtime-ktx库的一部分。因此,简化的解决方案如下所示:

// build.gradle
dependencies {
    ...
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc02"
}
class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        doSomething()
    }

    private fun doSomething() = viewLifecycleOwner.lifecycleScope.launch {
        val data = withContext(Dispathers.IO) {
            getData()
        }

        val pref = context!!.getSharedPreferences("mypref", MODE_PRIVATE)
        pref.edit().putBoolean("done", true).apply()
    }
}

还有其他有用的实用程序可以简化协程的使用,请查看Use Kotlin coroutines with Architecture components文档部分。

答案 1 :(得分:0)

这是预期的行为,因为协程取消本质上是合作的。

请参见https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#cancellation-is-cooperative

在您的情况下,虽然suspend函数将检查协程的状态,但您必须自己执行此操作。

一种更干净的方法是在视图模型中进行这些操作,然后在ViewModel onCleared中取消协程;如果您使用的是ViewModel

答案 2 :(得分:0)

最新的协程更新引入了使用示波器安全工作的可能性。基本上,如果您要在Fragments或Activity中进行异步工作,请使用lifecycleScope,如果要在ViewModel中进行异步工作,则需要使用viewModelScope。还有一个GlobalScope可以在任何类中使用,但事实证明它具有许多稳定性缺陷,应避免使用。在任何情况下,都应该使用lifecycleScope或viewModelScope并在其他类中使用暂停函数。

所以这是这样做的方法:

 override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    lifecycleScope.launch {
        doSomething()
    }
 }

 private suspend fun doSomething(){
    withContext(Dispatchers.IO){
        getData()
        if (isActive) {
            val pref = context!!.getSharedPreferences("mypref", MODE_PRIVATE)
            pref.edit().putBoolean("done", true).apply()
        }
    }
 }

如您在上面的代码中看到的,doSomething函数不需要作用域或启动,因为它充当在onActivityCreated中已启动的协程中执行的任务。在doSomething()函数中启动新的协程是没有意义的。同样,您可以将整个代码封装在withContext()语句中,因为获取共享首选项也是在Dispatchers.IO上运行效果最好的事务。另外,您可以随时将上下文更改为所需的上下文,例如:

   withContext(Dispatchers.IO){
        getData()
        if (isActive) {
            val pref = context!!.getSharedPreferences("mypref", MODE_PRIVATE)
            pref.edit().putBoolean("done", true).apply()
        }
        withContext(Dispatchers.Default){
            // Heavy computing
        }
    }

始终记住,Dispatchers.IO最适合数据库事务,共享首选项和网络调用。如果您在forEach中进行诸如forEach之类的繁重计算以及诸如此类的工作,请使用Dispatchers.Default。如果您需要查看工作,请使用Dispatchers.Main。

这些作用域确保您的任务被执行而不会被其他活动组件中断,并且如果它被诸如活动销毁之类的干扰所中断,它可以防止异常并在范围内的所有内容上正确使用垃圾收集器。自从我开始使用示波器以来,我再也没有崩溃了。