【问题标题】:Cancelling a callback inside a suspendCancellableCoroutine在suspendCancellableCoroutine中取消回调
【发布时间】:2021-01-24 10:20:56
【问题描述】:

我已经在suspendCancellableCoroutine中封装了一个回调来将它转换为一个挂起函数:

suspend fun TextToSpeech.speakAndWait(text: String) : Boolean {
    val uniqueUtteranceId = getUniqueUtteranceId(text)
    speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)
    return suspendCancellableCoroutine { continuation ->
        this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
            override fun onDone(utteranceId: String?) {
                if(utteranceId == uniqueUtteranceId) {
                    Timber.d("word is read, resuming with the next word")
                    continuation.resume(true)
                }
            }
        })
    }
}

我正在使用片段的 lifecycleScope 协程范围调用此函数,并且我假设它在片段被销毁时被取消。但是,LeakCanary 报告说我的片段因为这个监听器而泄漏,我用日志验证了即使在协程被取消后也调用了回调。

所以似乎用suspendCancellableCoroutine而不是suspendCoroutine包装并不足以取消回调。我想我应该主动检查作业是否处于活动状态,但是如何?我尝试了coroutineContext.ensureActive() 并在回调中检查了coroutineContext.isActive,但 IDE 给出了一个错误,提示 “只能在协程主体内调用暂停函数”我还能做些什么来确保它不会如果作业被取消,是否继续?

【问题讨论】:

  • 你有continuation.invokeOnCancellation { callback.cancel() }
  • 谢谢,有帮助!我的回调没有取消方法,但我尝试在取消时删除侦听器并且它有效:continuation.invokeOnCancellation { this.setOnUtteranceProgressListener(null) } 没有更多泄漏!再次感谢!您想将其添加为答案吗?

标签: android callback kotlin-coroutines cancellation


【解决方案1】:

LeakCanary 报告说我的片段因为这个监听器而泄漏,我用日志验证了即使在协程被取消后也调用了回调。

是的,底层异步 API 不知道 Kotlin 协程,您必须使用它来显式传播取消。 Kotlin 专门为此目的提供了invokeOnCancellation 回调:

return suspendCancellableCoroutine { continuation ->
    this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() { 
        /* continuation.resume() */ 
    })
    continuation.invokeOnCancellation {
        this.setOnUtteranceProgressListener(null)
    }
}

【讨论】:

    【解决方案2】:

    除了接受的答案之外,我发现 continuation 对象也具有 isActive 属性。因此,或者我们可以在恢复之前检查协程在回调中是否仍然处于活动状态:

        return suspendCancellableCoroutine { continuation ->
            this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() 
            {
                override fun onDone(utteranceId: String?) {
                    if(utteranceId == uniqueUtteranceId) {
                        if (continuation.isActive) {
                            continuation.resume(true)
                        }
                    }
                }
            })
            continuation.invokeOnCancellation {
                this.setOnUtteranceProgressListener(null)
            }
        }
    

    【讨论】:

      【解决方案3】:

      如果您想删除 JeLisUtteranceProgressListener 而不管结果如何(成功、取消或其他错误),您可以改用经典的 try/finally 块:

      suspend fun TextToSpeech.speakAndWait(text: String) : Boolean {
          val uniqueUtteranceId = getUniqueUtteranceId(text)
          speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)
          return try { 
              suspendCancellableCoroutine { continuation ->
                  this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
                      override fun onDone(utteranceId: String?) {
                          if(utteranceId == uniqueUtteranceId) {
                              Timber.d("word is read, resuming with the next word")
                              continuation.resume(true)
                          }
                      }
              })
          } finally {
              this.setOnUtteranceProgressListener(null)
          }
      }
      

      【讨论】:

      • 你说得对,还是去掉监听器比较好。但是问题的主要问题是立即取消作业并防止调用片段泄漏。但我可以将其与其他建议的解决方案结合起来。谢谢!
      • @OyaCanlı 事情是因为您的延续处于暂停状态,它将立即通过(重新)抛出 CancellationException 并输入 finally 块来完成。在这种情况下,您真的不需要显式取消侦听器。
      • 啊哈!我明白了,你是对的!这也有效。感谢您的澄清!
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-19
      • 1970-01-01
      • 1970-01-01
      • 2017-11-04
      • 1970-01-01
      • 2017-04-05
      相关资源
      最近更新 更多