本文区分原子变量、复合操作、并发数量与状态所有权四类问题,比较Atomic、Mutex、Semaphore和状态封闭的适用边界,并结合计数、缓存和限流场景,建立

把回调API包装成协程API,并不只是把回调内容搬进一个构建器。真正需要转换的是异步协议:一次调用究竟产生一个最终结果、持续产生多个事件,还是由多个并发生产者共同生成数据。

协议判断错误时,代码即使能够运行,也会隐藏结果丢失、重复恢复、监听器泄漏和顺序冲突。稳定的桥接方案应同时定义结果数量、失败通道、取消传播、资源释放以及多来源结果的优先级。

内容摘要

  1. 单次最终结果使用suspendCancellableCoroutine,成功和失败只能完成一次。
  2. 持续回调使用callbackFlow,通过trySend发送数据,并检查发送结果。
  3. awaitClose负责在收集取消或Flow关闭时注销监听器、释放资源。
  4. 多个协程并发生产数据使用channelFlow,但结果顺序和覆盖规则仍由业务定义。
  5. 桥接层必须让协程取消尽量传递到底层请求,形成完整生命周期闭环。

一、桥接之前先确认结果协议

外部API常见的三种结果模型是:

复制代码
一次请求返回一个最终结果
持续监听并产生多个事件
多个异步来源并发产生结果

它们分别对应不同抽象:

复制代码
单次结果       → suspendCancellableCoroutine
持续事件       → callbackFlow + awaitClose
多协程并发生产 → channelFlow

这里的关键不是回调函数数量,而是一次订阅期间允许产生多少个有效结果。一个接口即使同时提供onSuccessonError,只要二者互斥地表达一次最终完成,仍然属于单次结果。

反过来,状态从"已提交"变化到"传输中",最后再进入"成功"或"失败",已经是一个时间序列。强行压成一次返回值会丢失中间状态,也无法表达后续更新。

本节小结

回调桥接首先是协议建模。结果数量决定使用挂起函数还是Flow,生产者数量再决定是否需要channelFlow

二、单次结果如何安全转换为挂起函数

kotlin 复制代码
suspend fun awaitLocation(): Location =
    suspendCancellableCoroutine { continuation ->
        // 把外部单次回调映射成一次挂起函数调用
        val callback = object : LocationCallback {
            override fun onSuccess(value: Location) {
                // 成功回调恢复协程,并把 value 作为函数返回值
                continuation.resume(value)
            }
​
            override fun onError(error: Throwable) {
                // 失败回调恢复协程,并让调用方收到异常
                continuation.resumeWithException(error)
            }
        }
​
        // 注册回调后,当前协程保持挂起等待最终结果
        locationClient.request(callback)
​
        continuation.invokeOnCancellation {
            // 调用方取消时,尽量同步取消底层请求并解除引用
            locationClient.cancel(callback)
        }
    }

这段代码建立了三条转换路径:

scss 复制代码
回调成功 → resume(value)             → 挂起函数返回
回调失败 → resumeWithException(error) → 挂起函数抛出异常
协程取消 → invokeOnCancellation       → 取消请求或注销回调

Continuation只能完成一次

挂起函数一次调用只能返回一个值或抛出一个异常。底层SDK如果重复触发最终回调,第二次调用resume就会出错。

如果SDK可能重复回调,需要确保只有第一次最终结果生效:

kotlin 复制代码
// 多个回调同时到达时,AtomicBoolean 只允许其中一个把状态改为 true
val completed = AtomicBoolean(false)
​
fun complete(block: () -> Unit) {
    if (completed.compareAndSet(false, true)) {
        // 只有第一次从 false 更新为 true 的调用能够执行完成逻辑
        block()
    }
}

第一条最终结果完成Continuation,后续重复结果忽略并记录。如果同一请求先成功后失败,这不是普通重复,而是协议冲突;应结合请求标识、状态机和日志定位来源,不能用"最后一次结果覆盖前一次"处理。

取消后的竞态仍需考虑

取消和回调可能几乎同时发生。suspendCancellableCoroutine提供可取消Continuation,但底层资源不会因此自动停止。invokeOnCancellation必须尽量调用SDK的取消请求或反注册接口。

清理逻辑还应具备幂等性,因为正常完成、主动取消和SDK自身结束都可能触发相邻的资源释放路径。

工程场景:厂商SDK返回重复或相互矛盾的结果

部分设备SDK可能在网络切换、内部重试或跨进程恢复后产生异常回调序列:

复制代码
同一requestId连续回调两次成功
先回调成功,随后又回调失败
页面退出并取消协程后,旧回调仍然到达
新请求启动后,旧请求结果错误写入新页面

只检查isActive并不能防止重复恢复。检查结束后,协程可能立即被取消;两个回调也可能几乎同时到达。比较稳妥的做法,是另外记录这次请求是否已经结束:只有第一个结果可以恢复协程,后续结果只记录日志。

kotlin 复制代码
private enum class TerminalState {
    // 显式记录第一次被接受的最终状态,便于识别冲突回调
    Success,
    Failure
}
​
suspend fun awaitDeviceResult(
    requestId: String
): DeviceResult = suspendCancellableCoroutine { continuation ->
    // null 表示请求还没有收到最终结果
    val terminal = AtomicReference<TerminalState?>(null)
​
    val callback = object : DeviceCallback {
        override fun onSuccess(
            callbackRequestId: String,
            value: DeviceResult
        ) {
            // 丢弃其他请求的迟到回调,防止旧结果污染当前调用
            if (callbackRequestId != requestId) {
                logger.warn("ignore mismatched request")
                return
            }
​
            // 只有第一个回调能够把请求标记为成功
            val accepted = terminal.compareAndSet(
                null,
                TerminalState.Success
            )
            if (!accepted) {
                logger.error(
                    "conflicting terminal callback: " +
                        "${terminal.get()} -> success"
                )
                return
            }
​
            // 正常完成路径也要注销监听器,不能只在取消时清理
            deviceSdk.unregister(this)
​
            if (continuation.isCancelled) {
                // 协程已经取消,迟到结果只记录,不再恢复业务代码
                logger.warn("late success after cancellation")
                return
            }
​
            // 前面的 CAS 保证这里最多执行一次
            continuation.resume(value)
        }
​
        override fun onError(
            callbackRequestId: String,
            error: Throwable
        ) {
            // 错误结果也必须先验证所属请求
            if (callbackRequestId != requestId) return
​
            // 成功和失败都在争取第一次完成这次请求
            val accepted = terminal.compareAndSet(
                null,
                TerminalState.Failure
            )
            if (!accepted) {
                logger.error(
                    "conflicting terminal callback: " +
                        "${terminal.get()} -> failure"
                )
                return
            }
​
            // 请求结束后立即解除 SDK 对 callback 的持有
            deviceSdk.unregister(this)
​
            if (continuation.isCancelled) {
                logger.warn("late failure after cancellation")
                return
            }
​
            // 让挂起函数以异常结束,交由调用方处理失败
            continuation.resumeWithException(error)
        }
    }
​
    // requestId 同时用于发起请求和校验回调归属
    deviceSdk.start(requestId, callback)
​
    continuation.invokeOnCancellation {
        // 取消请求与注销回调都必须支持并发或重复调用
        deviceSdk.cancel(requestId)
        deviceSdk.unregister(callback)
    }
}

这段适配代码明确处理了四个边界:

  • requestId隔离不同请求,避免旧结果污染新调用。
  • AtomicReference保证恢复方法最多调用一次,并识别重复或相互矛盾的回调。
  • CancellableContinuation保证并发的取消与单次恢复只有一方成功。
  • 正常完成和取消路径都注销回调,invokeOnCancellation还会尽量终止底层工作。

注销与取消接口必须允许并发或重复调用,因为回调与取消可能同时发生。日志还可以记录时间、设备型号和SDK版本,帮助判断问题来自业务调用、厂商实现还是进程切换。适配代码需要保护协程调用方,但不能把SDK返回了矛盾结果这件事悄悄隐藏起来。

本节小结

单次回调桥接的完整契约是:成功返回、失败抛出、取消清理、只完成一次。缺少任意一项,都只是语法转换,还没有完成生命周期转换。

三、持续回调如何转换为Flow

kotlin 复制代码
fun locationUpdates(): Flow<Location> = callbackFlow {
    // callbackFlow 适合一次订阅期间持续产生多个回调结果
    val listener = object : LocationListener {
        override fun onLocation(value: Location) {
            // 普通回调不能调用挂起的 send,因此使用非挂起 trySend
            trySend(value)
                .onFailure { cause ->
                    // 发送失败表示数据没有进入 Flow,必须显式记录或处理
                    logger.warn("location delivery failed", cause)
                }
        }
​
        override fun onError(error: Throwable) {
            // 外部数据源失败时,携带异常关闭 Flow
            close(error)
        }
    }
​
    // 开始收集时注册外部监听器
    locationClient.register(listener)
​
    awaitClose {
        // 收集取消或 Flow 关闭后注销监听器,形成资源闭环
        locationClient.unregister(listener)
    }
}

callbackFlow内部使用Channel接收来自外部回调的数据,因此回调可以从不同线程安全地提交结果。普通回调不是挂起函数,通常不能直接调用挂起的send(),所以使用非挂起的trySend()

trySend成功不等于消费完成

trySend(value)成功只表示数据已被Channel接受,不代表下游业务已经处理完成。失败则表示数据没有进入Flow,返回的ChannelResult必须被检查:

vbnet 复制代码
解析或转换代码抛异常 → try-catch处理
数据未能进入Channel  → ChannelResult.onFailure处理
外部数据源终止并报错 → close(error)传递给下游

不能只用try-catch包裹trySend后就认为发送失败已被处理,因为发送失败通常通过结果对象表达。

awaitClose建立资源闭环

注册监听器以后,生产端需要保持有效,直到下游停止收集或Flow主动关闭。awaitClose同时承担等待和最终清理:

arduino 复制代码
开始收集
→ 注册监听器
→ 多次trySend
→ 收集取消或close
→ awaitClose执行
→ 注销监听器

如果省略注销,页面即使不再收集,SDK仍可能持有监听器并持续回调,最终造成资源浪费或对象泄漏。

本节小结

callbackFlow负责把持续回调转换为数据流,trySend负责非挂起发送,close(error)负责结束异常通道,awaitClose负责生命周期收尾。四者共同构成完整桥接。

四、多个生产者如何汇入同一条Flow

普通flow {}强调同一个生产协程按顺序调用emit。当多个子协程需要并发产生数据时,可以使用channelFlow

kotlin 复制代码
fun userSources(): Flow<UserResult> = channelFlow {
    // 两个子协程可以并发生产,并安全汇入同一条 Flow
    launch {
        // 缓存结果可能更快到达,但不代表业务优先级更高
        send(UserResult.Cache(database.loadUser()))
    }

    launch {
        // 网络结果完成顺序不确定,下游仍需制定覆盖规则
        send(UserResult.Network(api.loadUser()))
    }
}

channelFlow保证并发发送的安全性,却不保证缓存和网络谁先完成:

复制代码
安全汇合 ≠ 业务有序

如果网络数据必须覆盖缓存,结果模型至少需要携带来源、版本或时间戳,并由明确规则决定是否接纳。不能把协程完成顺序直接当作业务优先级。

channelFlow也不应替代所有普通Flow。单一生产者顺序发射时,flow {}的语义更直接;外部持续回调使用callbackFlow更能表达注册与注销关系。

本节小结

channelFlow解决多协程并发发送的安全汇合,业务层仍需解决顺序、去重和覆盖优先级。并发容器不会自动生成正确的数据策略。

结语:桥接的是协议,也是生命周期

从回调到协程的选择模型可以归纳为:

复制代码
一个最终结果 → suspendCancellableCoroutine
持续多个事件 → callbackFlow + awaitClose
并发多个来源 → channelFlow + 明确合并规则

无论选择哪一种抽象,桥接层都必须完整表达成功、失败、取消和清理。协程API的价值不只在于消除回调嵌套,更在于把外部异步协议纳入可管理、可取消、可验证的生命周期结构。

参考资料

相关推荐
2501_915909061 小时前
iOS test 测试怎么做?功能、性能、兼容、稳定与安全五类测试指南
android·ios·小程序·https·uni-app·iphone·webview
赵广陆2 小时前
企业实战:Markdown图片检索
android·java·开发语言
alexhilton3 小时前
千万别误用Android Skills
android·kotlin·android jetpack
淡淡的香烟3 小时前
Android视频直播播放器简单封装
android·物联网·音视频
爱笑鱼4 小时前
Android 系统启动机制导读:从 init 到 system_server,谁创建谁?
android
爱笑鱼4 小时前
Android 系统启动机制(一):init 到底是什么?为什么它不是普通 Native Service?
android
必须会一定会4 小时前
GLM-5.3 迁移实战:强制思考、1M 上下文与官方编程基准
android·开发语言·kotlin
小孔龙4 小时前
Android MessageQueue:从单锁队列到 DeliQueue
android
math_hongfan4 小时前
事务下单与状态流转:ArkTS 的 JOIN 联表在鸿蒙订单里实战
android·学习·华为·harmonyos