kotlin协程 容易被忽视的CompletableDeferred

CompletableDeferred是一个 可手动完成 的 Deferred, 它实现了 Deferred(可以 await()),也提供了 complete(value) / completeExceptionally(e) / cancel() 等方法,并由外部触发结果。它创建后可能处于未完成状态,任意线程或协程都能把它完成或失败,等待方使用 await() 来获取结果。

一、与Deferred、suspendCancellableCoroutine和Channel的区别

  • vs Deferred(由 async 返回):async 的 Deferred 是由协程体自己完成;CompletableDeferred 允许外部完成。

  • vs suspendCancellableCoroutine:两者都能把回调桥接到协程,suspendCancellableCoroutine 适合一次性封装回调;CompletableDeferred 更适合"外部多方在不同时间完成/通知"的场景(比如事件总线、跨协程信号、测试用的手动完成)。

  • vs Channel:Channel 是多次消息传递而CompletableDeferred 是单次结果(一次性)。

二、经典应用场景

1、桥接回调

一般的桥接回调推荐用suspendCancellableCoroutine,但如果外部多方的桥接推荐用CompletableDeferred。下面是一个简单的网络请求的例子。

kotlin 复制代码
suspend fun Call.awaitResponse(): Response = suspendCancellableCoroutine { cont ->
    enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            if (cont.isActive) cont.resumeWithException(e)
        }

        override fun onResponse(call: Call, response: Response) {
            //response记得use{}关闭
            if (cont.isActive) cont.resume(response) 
        }
    })
    cont.invokeOnCancellation { cancel() }
}

如果你想用 CompletableDeferred(当回调可能多次触发或在别处完成时非常好):

kotlin 复制代码
fun makeRequest(): CompletableDeferred<String> {
    val deferred = CompletableDeferred<String>()
    httpClient.newCall(request).enqueue(object: Callback {
        override fun onFailure(call: Call, e: IOException) {
            deferred.completeExceptionally(e)
        }
        override fun onResponse(call: Call, response: Response) {
            val s = response.body?.string() ?: ""
            deferred.complete(s)
            response.close()
        }
    })
    return deferred
}

// 使用处
lifecycleScope.launch {
    try {
        val result = makeRequest().await()
    } catch (e: Throwable) { ... }
}

上面的桥接肯定推荐suspendCancellableCoroutine来完成,只是为了展示CompletableDeferred也可以完成回调的桥接。下面看一个多处回调桥接的例子:

kotlin 复制代码
// 使用 CompletableDeferred 桥接多回调
suspend fun wakeUpDevice(): Boolean = coroutineScope {
    val deferred = CompletableDeferred<Boolean>()
    val finished = AtomicBoolean(false)
    val failCount = AtomicInteger(0)

    // 本地唤醒
    sendLocalWakeUp(
        onSuccess = {
            if (finished.compareAndSet(false, true)) {
                deferred.complete(true)
            }
        },

        onFail = {
            if (failCount.incrementAndGet() == 2) {
                deferred.complete(false)
            }
        }
    )

    // 云端唤醒
    sendCloudWakeUp(
        onSuccess = {
            if (finished.compareAndSet(false, true)) {
                deferred.complete(true)
            }
        },

        onFail = {
            if (failCount.incrementAndGet() == 2) {  
                deferred.complete(false)            
            }
        }
    )

    // 统一 suspend 等待结果
    deferred.await()
}

  


// 示例调用
fun main() = runBlocking {
    val result = wakeUpDevice()
    println("唤醒结果:$result")
}

上面就是多处回调桥接的例子,只要有一次成功就成功,都失败才算失败。

2、多生产者单消费者"谁先到就用谁"

complete函数只能被调用一次,后续调用无效,基于这个特性可以完成多生产者单消费者-谁先到就用谁的需求。一个简单的示例如下:

kotlin 复制代码
//两个异步来源(A、B)谁先返回就用谁的结果
val winner = CompletableDeferred<String>()
fun sourceA() { /* 异步获取 */ winner.complete("fromA") }
fun sourceB() { /* 异步获取 */ winner.complete("fromB") }
lifecycleScope.launch {
    val result = winner.await() // 谁先 complete 就是结果
}

3、协程间通知

这是一个比较巧妙的用法,例如一个协程做准备工作,另一个协程等待完成:

kotlin 复制代码
val ready = CompletableDeferred<Unit>()

launch {
    prepareResources()
    ready.complete(Unit)
}

launch {
    ready.await() //挂起阻塞,等待准备(prepareResources)完成
    startWork()  //另一个协程的执行部分
}

扩展思维:ready.complete(Unit)还可以在用户输入后,点击按钮后等,可以发挥想象。

4、与select、超时结合

select 表达式同时等待多个挂起操作采用第一个完成的结果。

kotlin 复制代码
fun CoroutineScope.fetchWithFallbacks(): CompletableDeferred<String> {
    val result = CompletableDeferred<String>()

    launch {
        try {
            val data = select<String> {
                primarySource().onAwait { it }
                secondarySource().onAwait { it }
                onTimeout(800) { throw TimeoutException("Timeout") }
            }
            result.complete(data)
        } catch (e: Exception) {
            // 启动异步恢复任务(不立即completeExceptionally)
            launch {
                delay(1000)
                try {
                    val recovery = recoverySource()
                    result.complete(recovery)
                } catch (e2: Exception) {
                    result.completeExceptionally(e2) // 真正失败
                }
            }
        }
    }

    return result
}

三、取消与异常传播

  • 如果 CompletableDeferred 被 cancel(),等待的协程会收到 CancellationException。

  • 如果在完成前等待者被取消,完成者仍然可以 complete() ------ 但 await() 不会再返回值(因为等待者已被取消)。

  • completeExceptionally(e) 会使 await() 抛出相同异常。

  • complete() 返回 true 或 false(表示是否第一次完成)。

四、注意事项与常见坑

  1. 不要泄漏未完成的 CompletableDeferred:把它长期暴露给全局可能会导致内存泄漏,尤其当负责完成它的组件被销毁时。

  2. 只在受信任的地方完成:不要让多个地方都可能完成且没序的代码混乱;若有竞态,记得检查 complete 的返回值。

  3. 避免把 CompletableDeferred 当作长生命周期队列:它是一次性完成的;若需要多次事件,请用 Channel / SharedFlow。

  4. 取消处理:如果等待者可能取消,使用 invokeOnCompletion 或 invokeOnCancellation 做清理(比如取消底层请求)。

  5. 完成后不要反复设置结果:complete 多次调用只有第一次有效,后续会返回 false。

  6. 与协程作用域的关系:CompletableDeferred 本身不是 Job,但它有 asJob() 可用于组合;也可以传入父 Job 构造(CompletableDeferred(parentJob))以便取消联动。

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui