You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在suspendCancellableCoroutine中处理回调取消避免Fragment泄漏?

解决TextToSpeech挂起函数的协程取消与泄漏问题

你遇到的问题很典型:用suspendCancellableCoroutine包装回调时,只依赖协程自动取消是不够的——因为回调是由TextToSpeech系统触发的,即使协程取消了,回调仍然可能被调用,进而持有Fragment的引用导致泄漏。而且回调方法本身不是挂起函数,所以不能直接调用coroutineContext.ensureActive()这类挂起函数。

下面是几个可行的解决方案,结合起来就能彻底解决问题:

1. 在回调中检查Job状态

既然无法在回调里直接访问协程上下文,我们可以提前捕获当前协程的Job,然后在回调中检查它是否还活跃,只有活跃时才恢复协程:

suspend fun TextToSpeech.speakAndWait(text: String): Boolean {
    val uniqueUtteranceId = getUniqueUtteranceId(text)
    // 捕获当前协程的Job,用于后续检查状态
    val currentJob = coroutineContext[Job]
    speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)

    return suspendCancellableCoroutine { continuation ->
        val listener = object : JeLisUtteranceProgressListener() {
            override fun onDone(utteranceId: String?) {
                if (utteranceId == uniqueUtteranceId) {
                    // 只有Job仍活跃时,才恢复协程
                    if (currentJob?.isActive == true) {
                        Timber.d("word is read, resuming with the next word")
                        continuation.resume(true)
                    } else {
                        Timber.d("Coroutine has been cancelled, skipping resume")
                    }
                }
            }

            // 别忘了处理错误场景,避免协程一直挂起
            override fun onError(utteranceId: String?) {
                if (utteranceId == uniqueUtteranceId && currentJob?.isActive == true) {
                    continuation.resume(false)
                }
            }
        }

        this.setOnUtteranceProgressListener(listener)

        // 协程取消时的关键清理逻辑
        continuation.invokeOnCancellation {
            // 移除监听,避免后续回调触发并持有引用
            this.setOnUtteranceProgressListener(null)
            // 主动停止TTS播放,从根源上阻止回调继续执行
            stop()
            Timber.d("Coroutine cancelled, cleaned up TTS resources")
        }
    }
}

2. 利用invokeOnCancellation做资源清理

这一步是核心:在suspendCancellableCoroutine中通过continuation.invokeOnCancellation注册取消回调,当协程被取消时(比如Fragment销毁时lifecycleScope自动取消协程),主动停止TextToSpeech的播放,并移除UtteranceProgressListener。这样既可以避免不必要的回调触发,又能切断Listener对Fragment的引用链,防止泄漏。

3. 配合LifecycleScope的自动取消

因为你是在lifecycleScope中调用这个挂起函数,当Fragment销毁时,lifecycleScope的协程会自动触发取消逻辑,此时上面的invokeOnCancellation代码就会执行,完成TTS资源的清理;同时回调里的Job状态检查,也会阻止已经取消的协程被意外恢复。

另外补充个小细节:如果你的JeLisUtteranceProgressListener是抽象类,建议把所有回调方法(比如onStart)都做基础处理,避免遗漏场景导致协程挂起或资源泄漏。

内容的提问来源于stack exchange,提问作者Oya Canli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 14:52:28