【问题标题】:Scope confused in coroutines协程中的范围混淆
【发布时间】:2019-04-27 14:23:35
【问题描述】:

我有一个用例,我想使用协程,但有点困惑如何实现它。

具有范围并绑定到 UI 生命周期并从存储库调用 API 的 ViewModel:

class UserViewModel(): CoroutineScope {

    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    fun showUser() { 
       launch {
          val user = repo.getUser() 
          livedata = user
       }
    }

    fun onClean() {
       job.cancel()
    }
}

存储库使用协程来构建网络调用,如下所示:

suspend fun getUser() = GlobalScope { ... }

用例是,一旦从 ViewModel 调用 API,就需要始终完全执行存储库功能,因为我们需要捕获来自服务器的所有网络响应。

如何确保存储库中的协程始终执行但 ViewModel 协程将被取消以避免视图模型被清除后的内存泄漏?

【问题讨论】:

  • 为什么必须完成getUser 操作?它是只读的。
  • getUser 正在执行网络请求,但无论在 viewmodel 中如何处理响应,我们都需要捕获所有异常并在必要时在此函数中改变应用状态

标签: android kotlin kotlin-coroutines coroutine coroutinescope


【解决方案1】:

根据GlobalScope 的文档,我认为我们可以依靠使用全局 CoroutineScope 启动的协程始终执行。文档说:

全局作用域用于启动在整个应用程序生命周期内运行且不会提前取消的顶级协程。

我已经实现了一些测试代码,当jobUserViewModel 中被取消时,存储库中的协程继续执行。这是我的 cmets 的代码:

class UserViewModel(): CoroutineScope {
    private val job = Job()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    fun showUser() {
        launch {
            val repo = Repository()
            val userDeferred = repo.getUser()
            // if onClean() is called before the coroutine in Repository finishes,
            // this line will not be called, but coroutine in Repository will continue executing
            val result = userDeferred.await() // wait for result of I/O operation without blocking the main thread
        }
    }

    fun onClean() {
        job.cancel()
    }
}

class Repository {
    fun getUser() = GlobalScope.async {
        delay(4000)
        // this line is executed no matter whether the job in UserViewModel was canceled or not
        "User returned"
    }
}

另外我们可以减少showUser()函数:

fun showUser() = repo.getUser().then(this) {
    // `it` contains the result
    // here is the main thread, use `it` to update UI
}

使用扩展函数then:

fun <T> Deferred<T>.then(scope: CoroutineScope = GlobalScope, uiFun: (T) -> Unit) {
    scope.launch { uiFun(this@then.await()) }
}

如果您为 Android 开发并希望确保您的 IO 操作即使在清理 ViewModel 后也能完全执行,请使用WorkManager。它适用于需要保证即使应用程序退出系统也会运行它们的异步和可延迟任务。

【讨论】:

  • await 会阻塞协程吗?这意味着我不能用 dispatchers.main 做到这一点?
  • await 会阻塞协程但不会阻塞main thread,你可以和Dispatchers.Main一起使用
  • await 阻塞协程有点误导。你应该说它暂停协程。
【解决方案2】:

ViewModel 只能在配置更改后幸存下来,而不能在一般活动的破坏中幸存下来。

如果您希望您的操作在 Activity 销毁之后继续进行,您应该使用生命周期超出 Activity 生命周期的组件,即 Service

此外,如果您想确保您的操作“始终”执行,您应该使用前台服务,该服务需要在服务运行时发出不可关闭的通知。

已启动的服务可以使用startForeground(int, Notification) API 将服务置于前台状态,系统认为它是用户主动意识到的,因此在内存不足时不会被终止。

【讨论】:

  • 不适用于我的情况,不推荐该服务用于轻量异步操作。
  • 异步操作应在其组件被销毁时取消。如果您希望某些事情总是完成,您应该使用永远不会被破坏的组件。在组件的生命周期之外进行异步操作是内存泄漏的一个接收器
  • 如果repositories是singleton并且我们使用GlobalScope,那么在启动repositories功能的viewmodels被清除后怎么会出现泄漏?
  • 这取决于但一般来说这是一种不好的做法,应该避免medium.com/@elizarov/…
猜你喜欢
  • 2020-06-13
  • 2020-02-16
  • 1970-01-01
  • 2011-12-22
  • 1970-01-01
  • 1970-01-01
  • 2020-05-21
  • 1970-01-01
  • 2023-03-09
相关资源
最近更新 更多