从 Callback 到 Coroutines:Android 异步并发方案的演进

455 阅读5分钟

前言:同时发起 A、B、C 三个网络请求,全部完成后再串行请求 D —— 这是移动端最常见的并发场景之一。本文通过完整可运行的代码,横向对比 5 种主流实现方案,带你看清各自的本质与取舍。


📌 场景定义

[请求 A] ─┐
[请求 B] ─┼─ (并行) ──▶ 全部完成 ──▶ [请求 D] ──▶ 拿到最终结果
[请求 C] ─┘

这个"3并1串"的模式在真实业务中极为常见,比如:首页同时拉取轮播图、文章列表、置顶文章,全部就绪后再根据结果请求个性化推荐。


方案一:Java Callbacks(嵌套回调)

实现代码

image.png

⚠️ 核心问题

注意:这段代码虽然嵌套了 4 层,但 A、B、C 三个请求实际上是串行的,并非真正的并行,要在纯 Callback 模式下实现并行,你还需要额外维护一个计数器或 AtomicInteger,代码复杂度会进一步爆炸。

维度评估
并行实现❌ 需要手动计数器或者JUC,极易出错
代码可读性❌ 向右无限延伸,逻辑碎片化
错误处理❌ 每层都要重复 onFailure
适用场景仅适合单次简单异步

方案二:CountDownLatch(显式锁阻塞)

实现代码

image.png

⚠️ 核心问题

latch.await() 会让后台线程进入 Blocked(阻塞) 状态,线程被占用但什么工作也不做。在主线程等待,这会导致ANR。

更危险的是:任何一个分支如果忘记调用 countDown()(例如某个异常路径没覆盖到),整个流程将永久死等

维度评估
并行实现✅ 真正并行
线程状态❌ 阻塞(Blocked),浪费资源
健壮性❌ 遗漏 countDown = 死锁
代码复杂度中等

方案三:CompletableFuture(链式异步)

实现代码

image.png

⚠️ 核心问题

逻辑链路清晰了许多,但有两个隐患:

  1. 调试地狱:异常堆栈在异步链条中会丢失调用上下文,出了问题很难定位到具体是哪一步。
  2. 心智负担thenApply / thenCompose / thenCombine / allOf —— 你需要熟练掌握这些操作符的区别才能正确使用。
维度评估
并行实现✅ 真正并行
线程状态✅ 非阻塞(线程池驱动)
调试体验❌ 异步链中堆栈丢失
API 学习成本中等偏高

方案四:RxJava3(响应式流)

实现代码

image.png

⚠️ 核心问题

RxJava 非常强大,但在这个场景下有"大炮打蚊子"之嫌:

  1. 框架重量级:引入 RxJava3 + RxAndroid,仅为处理简单的顺序业务逻辑,成本偏高。
  2. 生命周期管理subscribe 返回的 Disposable 必须在 onDestroy 中手动 dispose(),否则会内存泄漏。
// 不处理 Disposable = 内存泄漏!
val disposable = observable.subscribe(...)
// 必须在 onDestroy 中:
disposable.dispose()
维度评估
并行实现✅ 真正并行(zip 操作符)
表达力✅ 复杂数据流处理极强
框架重量❌ 引入成本高
生命周期❌ 需手动管理 Disposable

方案五:Kotlin Coroutines(协程 ⭐ 推荐)

实现代码

image.png

✅ 为什么协程是最佳答案

1. 挂起 ≠ 阻塞

await() 期间,线程不会被占用——协程被"挂起",线程可以去做其他事情。这与 CountDownLatch.await() 的死等有本质区别。

CountDownLatch:  线程 ──[Blocked 死等]──────────────▶ 继续
Coroutine:       线程 ──[释放去处理其他协程]──▶ 恢复继续

2. 结构化并发,生命周期自动管理

lifecycleScope.launch 会自动将协程的生命周期绑定到 Activity,当 Activity 销毁时,所有未完成的请求会自动取消,无需任何手动清理。

3. 用线性代码表达异步逻辑

上面的协程代码读起来就像同步代码,但底层完全是异步非阻塞的。这就是协程最核心的价值:用人类最自然的线性思维,编写高性能的异步逻辑

维度评估
并行实现✅ async/await
线程状态✅ 挂起(Suspended),不占资源
生命周期✅ 自动绑定,无需手动取消
代码可读性✅ 伪同步,极易维护
学习成本低(语言原生支持)

📊 五方案横向对比

方案真正并行等待机制线程状态生命周期管理代码复杂度可维护性
Callbacks嵌套触发运行/空闲手动极高
CountDownLatch显式锁阻塞Blocked手动⭐⭐
CompletableFuture回调链驱动运行(线程池)手动中高⭐⭐⭐
RxJava3操作符聚合运行(线程池)手动 Disposable⭐⭐⭐
Coroutines非阻塞挂起Suspended自动极低⭐⭐⭐⭐⭐

💡 总结

每一种方案的演进,都是在解决上一代的痛点:

  • Callbacks —— 最原始,让代码横向无限延伸,并行更是一场噩梦。
  • CountDownLatch —— 解决了并行,但用阻塞线程换来的,有ANR和死锁风险。
  • CompletableFuture —— 链式调用更优雅,但调试困难,API 理解成本不低。
  • RxJava3 —— 对复杂数据流极其强大,但在简单场景下是过度设计,生命周期还要自己管。
  • Kotlin Coroutines —— 消灭了锁,消灭了回调,让代码回归线性,阅读起来顺畅。

GitHub