Kotlin Coroutine
以图片处理串行流水线为例,对比 Callback、RxJava 与 Coroutines 三种异步写法
串行异步是移动端最常见的场景之一:前一步的输出是后一步的输入,任一步失败都应中断后续流程并向用户反馈。当链路从 3 步扩展到 10 步,写法的选择直接决定代码的可读性与可维护性。
本文以一个典型的图片处理流水线为例,对比 Callback、RxJava、Kotlin Coroutines 三种实现方式,说明 Coroutines 如何用「同步写法」表达「异步执行」。
一、业务场景:串行图片处理流水线
用户从相册选择一张图片后,App 需依次完成三步 IO 密集型操作:
| 步骤 | 操作 | 输入 | 输出 | 失败处理 |
|---|---|---|---|---|
| 0 | 用户选图 | — | originBitmap | — |
| 1 | PictureCompress 压缩 | 原图 | afterCompressBitmap | Toast「压缩失败」 |
| 2 | PictureMatting 抠图 | 压缩图 | afterMattingBitmap | Toast「抠图失败」 |
| 3 | PictureFilter 滤镜 | 抠图结果 | afterFilterBitmap | Toast「滤镜失败」 |
| 4 | 展示 | 最终 Bitmap | UI 渲染 | — |
三步操作严格串行——压缩未完成不能抠图,抠图未完成不能加滤镜。每一步只有 Success / Fail 两种结果,Fail 时流程终止。

上图绿色路径为成功主线,红色虚线为失败分支。右侧三种写法解决的是同一个流程,差异在于代码组织方式与错误传播模型。
架构约束
以下代码均为伪代码,仅演示异步写法差异。实际项目中应:
- 将 IO 操作放在 Repository / UseCase,ViewModel 负责调度与状态暴露
- 遵循 SSOT / MVVM 分层,View 不直接发起网络或磁盘操作
- 在
viewModelScope+Dispatchers.IO中执行挂起函数,UI 更新切回 Main
二、Callback:嵌套回调与「回调地狱」
2.1 状态模型
每一步需定义 Success / Fail 两种 UIState,三步即六套类型:
public abstract class CompressImageUIState {
public static class Success extends CompressImageUIState {
@NotNull private final Bitmap afterCompressBitmap;
// constructor + getter
}
public static class Fail extends CompressImageUIState {
@NotNull private final Throwable throwable;
}
}
// Matting、Filter 同理,各需一套 UIState2.2 接口与主流程
public interface ImageOperation {
void compressImage(@NotNull Bitmap bitmap, Consumer<CompressImageUIState> callback);
void mattingImage(@NotNull Bitmap bitmap, Consumer<MattingImageUIState> callback);
void filterImage(@NotNull Bitmap bitmap, Consumer<FilterImageUIState> callback);
}Bitmap originBitmap = getSelectedBitmap();
compressImage(originBitmap, compressState -> {
if (compressState instanceof CompressImageUIState.Success success) {
mattingImage(success.getAfterCompressBitmap(), mattingState -> {
if (mattingState instanceof MattingImageUIState.Success mattingSuccess) {
filterImage(mattingSuccess.getAfterMattingBitmap(), filterState -> {
if (filterState instanceof FilterImageUIState.Success filterSuccess) {
filterSuccess.getAfterFilterBitmap().show();
} else {
toast("加入滤镜失败");
}
});
} else {
toast("抠图失败");
}
});
} else {
toast("压缩图片失败");
}
});2.3 问题分析
| 问题 | 说明 |
|---|---|
| 嵌套深度 | N 步串行 → N 层回调嵌套,可读性随步骤线性下降 |
| 错误处理分散 | 每层独立 if-else,难以统一日志与重试策略 |
| 控制流不直观 | 「先 A 再 B 再 C」的语义被淹没在闭包中 |
| 取消困难 | 中途离开页面时,需手动取消各层进行中的任务 |
Callback 能工作,但步骤一多就陷入回调地狱(Callback Hell)——这正是 Rx 与 Coroutines 要解决的问题。
三、RxJava:声明式流组合
RxJava 用 flatMap 将串行步骤展平为一条数据流,错误统一走 onError:
interface ImageOperation {
fun compressImage(bitmap: Bitmap): Single<Bitmap>
fun mattingImage(bitmap: Bitmap): Single<Bitmap>
fun filterImage(bitmap: Bitmap): Single<Bitmap>
}val originBitmap = getSelectedBitmap()
imageOperation.compressImage(originBitmap)
.flatMap { compressed -> imageOperation.mattingImage(compressed) }
.flatMap { matted -> imageOperation.filterImage(matted) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result -> result.show() },
{ error ->
when (error) {
is PictureCompressException -> toast("压缩图片失败")
is PictureMattingException -> toast("抠图失败")
is PictureFilterException -> toast("加入滤镜失败")
}
}
)
.also { compositeDisposable.add(it) }相对 Callback 的改进
- 串行语义清晰:
flatMap链即流水线 - 错误集中处理:
onError一处捕获 - 线程切换声明式:
subscribeOn/observeOn
仍存在的成本
- 需理解 Single、flatMap、Scheduler、Disposable 等概念
- 生命周期需手动绑定
CompositeDisposable - 调试栈不如线性代码直观
异步范式演进背景见 从 Rx 到 Kotlin Coroutines。
四、Kotlin Coroutines:同步写法,异步执行
4.1 suspend 接口
将异步操作声明为 suspend 函数,调用方无需回调:
interface ImageOperation {
suspend fun compressImage(bitmap: Bitmap): Bitmap
suspend fun mattingImage(bitmap: Bitmap): Bitmap
suspend fun filterImage(bitmap: Bitmap): Bitmap
}底层可在 withContext(Dispatchers.IO) 中执行实际 IO,对调用方透明。
4.2 方式一:逐步 try/catch(细粒度错误处理)
适合每步失败需要不同 UI 反馈的场景:
viewModelScope.launch {
val compressed = try {
imageOperation.compressImage(originBitmap)
} catch (_: PictureCompressException) {
toast("压缩图片失败")
return@launch
}
val matted = try {
imageOperation.mattingImage(compressed)
} catch (_: PictureMattingException) {
toast("抠图失败")
return@launch
}
val filtered = try {
imageOperation.filterImage(matted)
} catch (_: PictureFilterException) {
toast("加入滤镜失败")
return@launch
}
filtered.show()
}代码结构与流程图一一对应:从上到下读下来,就是 Compress → Matting → Filter → Show。
4.3 方式二:CoroutineExceptionHandler(统一错误处理)
适合错误处理逻辑相似、步骤较多的场景:
viewModelScope.launch(
CoroutineExceptionHandler { _, throwable ->
when (throwable) {
is PictureCompressException -> toast("压缩图片失败")
is PictureMattingException -> toast("抠图失败")
is PictureFilterException -> toast("加入滤镜失败")
}
}
) {
val compressed = imageOperation.compressImage(originBitmap)
val matted = imageOperation.mattingImage(compressed)
val filtered = imageOperation.filterImage(matted)
filtered.show()
}注意:
CoroutineExceptionHandler仅捕获未在协程体内 try/catch 的异常。若某步已局部捕获,Handler 不会再次触发。
4.4 方式三:扩展函数链式调用
通过 Kotlin 扩展函数,主流程可进一步压缩为链式风格:
private suspend fun Bitmap.compress(): Bitmap =
imageOperation.compressImage(this)
private suspend fun Bitmap.matting(): Bitmap =
imageOperation.mattingImage(this)
private suspend fun Bitmap.filter(): Bitmap =
imageOperation.filterImage(this)
viewModelScope.launch(exceptionHandler) {
originBitmap
.compress()
.matting()
.filter()
.show()
}链式写法与 Rx 的 flatMap 链异曲同工,但无需操作符学习成本,且享受结构化并发的自动取消。
五、三种写法对比
| 维度 | Callback | RxJava | Coroutines |
|---|---|---|---|
| 串行表达 | 嵌套回调 | flatMap 链 | 顺序 suspend 调用 |
| 可读性(3 步) | 差 | 好 | 最好 |
| 可读性(10 步) | 极差 | 好 | 最好 |
| 错误处理 | 分散 if-else | 统一 onError | try/catch 或 ExceptionHandler |
| 线程切换 | 手动 post | Scheduler | withContext / Dispatcher |
| 生命周期 | 手动取消 | Disposable | viewModelScope 自动取消 |
| 学习曲线 | 低 | 高 | 中 |
六、选型建议
- 遗留 Java 模块:Callback 或 CompletableFuture 即可,不必强行改写
- 已有 Rx 投资的大型项目:继续 flatMap 链,或通过
kotlinx-coroutines-rx3桥接迁移 - 新 Kotlin / Jetpack 项目:优先 Coroutines + suspend,配合
viewModelScope与Dispatchers.IO
无论选哪种方案,Repository 层封装 IO 细节、ViewModel 层编排流程、View 层只渲染的分层原则不变。Coroutines 的价值不在于语法新奇,而在于让串行异步的业务语义与代码结构重新对齐。
七、总结
图片处理流水线是理解 Coroutines 的绝佳入门场景:
- Callback 暴露了嵌套与分散错误处理的问题
- RxJava 用流式 flatMap 解决了组合,但引入了响应式概念栈
- Coroutines 用
suspend让代码结构与流程图同构——读代码即读流程
用同步的方式写异步的代码。
更多调度器、Flow、并发模式见 Kotlin Coroutine 进阶。