如何在suspendCancellableCoroutine中处理回调取消避免Fragment泄漏?
你遇到的问题很典型:用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

