简体   繁体   English

关于 Kotlin 协程取消的问题

[英]Question about Kotlin Coroutine Cancellation

I have a code something like the following in my app我的应用程序中有一个类似于以下的代码

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()
    }
}

In production, I get many NPEs in doSomething( ) while accessing context .在生产中,我在访问contextdoSomething( ) 中得到了许多NPEs

My assumption was that the coroutine gets cancelled after calling cancel() in onDestroy() , so I didn't bother to check the context for a null value.我的假设是coroutineonDestroy()调用cancel()后被cancel() ,所以我没有费心检查空值的context But it looks like continues to execute even after cancel() is called.但看起来即使在调用cancel()之后continues执行。 I think it happens if the cancel() is called after completing the withContext and before resuming the coroutines .我认为如果在完成withContext和恢复coroutines之前调用cancel()会发生这种情况。

So I replaced doSomething() with the following.所以我用以下替换了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()
        }
    }

This fixes the crash.这修复了崩溃。

However, Is this an expected behavior or am I doing something wrong?但是,这是预期的行为还是我做错了什么? Kotlin's documentation isn't very clear about this. Kotlin 的文档对此不是很清楚。 And most of the examples online are like my original code.而且网上的大部分例子都像我原来的代码。

Your code assumes that the withContext() will stop execution when it returns, if the scope is cancelled, but actually it didn't, till version 1.3.0 of kotlin coroutines. 您的代码假定withContext()返回时将停止执行(如果取消了作用域,但实际上并没有取消),直到kotlin coroutines 1.3.0版本。 Here is GitHub issue . 这是GitHub 问题 I guess that you are using an earlier version of the library. 我猜您正在使用该库的早期版本。

I also recommend you use LifecycleScope instead of a custom scope. 我还建议您使用LifecycleScope而不是自定义范围。 It's part of lifecycle-runtime-ktx library. 它是lifecycle-runtime-ktx库的一部分。 So, simplified solution looks like: 因此,简化的解决方案如下所示:

// 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()
    }
}

There are other helpful utilities which simplifies coroutines usage, take a look at Use Kotlin coroutines with Architecture components documentation section. 还有其他有用的实用程序可以简化协程的使用, 请参阅将Kotlin协程与“架构组件”一起使用

This is the expected behaviour since Coroutine Cancellation is cooperative in nature. 这是预期的行为,因为协程取消本质上是合作的。

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

While suspend functions will check for the state of coroutine in your case you will have to do this yourself. 尽管暂停功能将根据您的情况检查协程的状态,但您必须自己执行此操作。

A cleaner way is to have these operations in a viewmodel and just cancel the coroutine in ViewModel onCleared ; 一种更干净的方法是将这些操作包含在视图模型中,而只需在ViewModel onCleared取消协程; if you are using ViewModel 如果您使用的是ViewModel

The latest coroutine updates have introduced the possibility of working safe with scopes. 最新的协程更新已引入了使用示波器安全工作的可能性。 Basically if you are doing async work in Fragments or Activities you use lifecycleScope and if you are going async work in ViewModel you need to use viewModelScope . 基本上,如果您要在Fragments或Activity中进行异步工作,请使用lifecycleScope ;如果要在ViewModel中进行异步工作,则需要使用viewModelScope There is also a GlobalScope that can be used in any class but it was proved to have many stability flaws and should be avoided. 还有一个GlobalScope可以在任何类中使用,但事实证明它具有许多稳定性缺陷,应避免使用。 In any case scenario you should use either lifecycleScope or viewModelScope and use suspend functions in other classes. 在任何情况下,都应使用lifecycleScope或viewModelScope并在其他类中使用暂停函数。

So here is the way to do it: 因此,这是执行此操作的方法:

 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()
        }
    }
 }

As you can see in the code above, there is no need for a scope or launch in the doSomething function because that acts as a task that gets executed in the already launched coroutine above in onActivityCreated. 如您在上面的代码中看到的,doSomething函数不需要作用域或启动,因为它充当在onActivityCreated中已启动的协程中执行的任务。 Launching a new coroutine in the doSomething() function is pointless. 在doSomething()函数中启动新的协程是没有意义的。 Also, you can encapsulate your entire code in your withContext() statement because getting shared preferences is also a transaction that works best on Dispatchers.IO. 同样,您可以将整个代码封装在withContext()语句中,因为获取共享首选项也是在Dispatchers.IO上运行效果最好的事务。 Alternatively you can change the context to the desired context anytime you want, like: 或者,您可以随时将上下文更改为所需的上下文,例如:

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

Always remember that Dispatchers.IO is optimised best for database transactions, shared preferences and networking calls. 永远记住,Dispatchers.IO针对数据库事务,共享首选项和网络调用进行了最佳优化。 If you do heavy computing like forEach in forEach and stuff like that, use Dispatchers.Default and if you need to do view work, use Dispatchers.Main. 如果您在forEach中执行诸如forEach之类的繁重计算以及诸如此类的工作,请使用Dispatchers.Default;如果需要进行查看工作,请使用Dispatchers.Main。

These scopes make sure your task gets executed without interruption from other activity components, and if it is interrupted by something like activity destruction, it prevents exceptions and uses garbage collector properly on everything that's inside the scope. 这些作用域确保您的任务被执行而不会被其他活动组件中断,并且如果它被活动破坏之类的事物中断,它将防止异常并在范围内的所有内容上正确使用垃圾回收器。 Since I started using the scopes, there were no more crashes for me. 自从我开始使用示波器以来,再也没有崩溃的机会了。

As Valery Katkov's answer is correct but you could do this to solve your problem without coroutine cancelation: 由于瓦列里·卡特科夫(Valery Katkov)的答案是正确的,但是您可以这样做,而无需协程取消:

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

        val pref = context?.getSharedPreferences("mypref", MODE_PRIVATE) ?: return@launch
        pref.edit().putBoolean("done", true).apply()
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM