返回 Java / Kotlin
Java / Kotlin
9 分钟阅读

Kotlin Coroutine

以图片处理串行流水线为例,对比 Callback、RxJava 与 Coroutines 三种异步写法

串行异步是移动端最常见的场景之一:前一步的输出是后一步的输入,任一步失败都应中断后续流程并向用户反馈。当链路从 3 步扩展到 10 步,写法的选择直接决定代码的可读性与可维护性。

本文以一个典型的图片处理流水线为例,对比 Callback、RxJava、Kotlin Coroutines 三种实现方式,说明 Coroutines 如何用「同步写法」表达「异步执行」。


一、业务场景:串行图片处理流水线

用户从相册选择一张图片后,App 需依次完成三步 IO 密集型操作:

步骤操作输入输出失败处理
0用户选图originBitmap
1PictureCompress 压缩原图afterCompressBitmapToast「压缩失败」
2PictureMatting 抠图压缩图afterMattingBitmapToast「抠图失败」
3PictureFilter 滤镜抠图结果afterFilterBitmapToast「滤镜失败」
4展示最终 BitmapUI 渲染

三步操作严格串行——压缩未完成不能抠图,抠图未完成不能加滤镜。每一步只有 Success / Fail 两种结果,Fail 时流程终止。

图片处理串行流水线:Compress → Matting → Filter,每步 Success 继续、Fail 中断并 Toast 提示;右侧对比 Callback、RxJava、Coroutines 三种实现风格

上图绿色路径为成功主线,红色虚线为失败分支。右侧三种写法解决的是同一个流程,差异在于代码组织方式错误传播模型

架构约束

以下代码均为伪代码,仅演示异步写法差异。实际项目中应:

  • 将 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 同理,各需一套 UIState

2.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 链异曲同工,但无需操作符学习成本,且享受结构化并发的自动取消。


五、三种写法对比

维度CallbackRxJavaCoroutines
串行表达嵌套回调flatMap 链顺序 suspend 调用
可读性(3 步)最好
可读性(10 步)极差最好
错误处理分散 if-else统一 onErrortry/catch 或 ExceptionHandler
线程切换手动 postSchedulerwithContext / Dispatcher
生命周期手动取消DisposableviewModelScope 自动取消
学习曲线

六、选型建议

  • 遗留 Java 模块:Callback 或 CompletableFuture 即可,不必强行改写
  • 已有 Rx 投资的大型项目:继续 flatMap 链,或通过 kotlinx-coroutines-rx3 桥接迁移
  • 新 Kotlin / Jetpack 项目:优先 Coroutines + suspend,配合 viewModelScopeDispatchers.IO

无论选哪种方案,Repository 层封装 IO 细节、ViewModel 层编排流程、View 层只渲染的分层原则不变。Coroutines 的价值不在于语法新奇,而在于让串行异步的业务语义与代码结构重新对齐。


七、总结

图片处理流水线是理解 Coroutines 的绝佳入门场景:

  • Callback 暴露了嵌套与分散错误处理的问题
  • RxJava 用流式 flatMap 解决了组合,但引入了响应式概念栈
  • Coroutinessuspend 让代码结构与流程图同构——读代码即读流程

用同步的方式写异步的代码。

更多调度器、Flow、并发模式见 Kotlin Coroutine 进阶

相关文章

Java / Kotlin
12 分钟
异步编程思想的演进:从 Rx 到 Kotlin Coroutines
从 Callback、Future 到 RxJava 与 Kotlin Coroutines——异步编程范式的演进脉络、设计哲学与选型依据
Java / Kotlin
14 分钟
Kotlin 协程原理详解(挂起 / 恢复 / 取消 / 调度 / 异常处理 全面梳理)
本文由ChatGPT生成