在 suspendCancellableCoroutine 中取消回调

Cancelling a callback inside a suspendCancellableCoroutine

我在 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 给出了一个错误,指出 "suspension functions can be called only within coroutine body" 什么如果作业被取消,我还能做些什么来确保它不会恢复?

LeakCanary reported that my fragment was leaking because of this listener and I verified with logs that the callback was called even after the coroutine is cancelled.

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

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

除了已接受的答案外,我还认识到 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)
        }
    }

如果您想删除 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)
    }
}