Kotlin 协程的七重境界:从 launch(IO) 到执行模型设计

很多 Android 开发者学习协程的成长路线几乎都一样。

从最开始:

能跑就行。

到最后:

开始设计异步边界、调度策略和数据流模型。

这其实是一个非常明显的认知升级过程。


第一境:launch(IO) 满天飞

刚接触协程的时候,大多数人的代码长这样:

kotlin 复制代码
button.setOnClickListener {
    CoroutineScope(Dispatchers.IO).launch {
        val user = api.getUser()

        withContext(Dispatchers.Main) {
            tv.text = user.name
        }
    }
}

或者:

kotlin 复制代码
GlobalScope.launch(Dispatchers.IO) {
}

项目里到处都是:

text 复制代码
launch(IO)
launch(IO)
launch(IO)
launch(IO)

特点:

  • 谁耗时谁 IO
  • 谁更新 UI 谁 Main
  • ViewModel、UseCase、Repository 全都切线程
  • 每层都觉得自己应该负责线程

结果:

text 复制代码
Main
 ↓
IO
 ↓
IO
 ↓
IO
 ↓
Main

线程切换次数比业务逻辑还多。

这一阶段的核心认知:

协程 = 开线程。

实际上:

协程从来不仅仅线程管理框架。


第二境:withContext() 满天飞

kotlin 复制代码
suspend fun loadUser(): User {
    return withContext(Dispatchers.IO) {
        api.getUser()
    }
}

然后:

kotlin 复制代码
suspend fun queryUser(): User {
    return withContext(Dispatchers.IO) {
        dao.query()
    }
}

再然后:

kotlin 复制代码
suspend fun updateUser() {
    withContext(Dispatchers.IO) {
        dao.insert()
    }
}

最后项目变成:

text 复制代码
withContext(IO)
withContext(IO)
withContext(IO)
withContext(IO)

甚至:

kotlin 复制代码
withContext(IO) {
    repository.load()
}

repository:

withContext(IO) {
    remote.load()
}

remote:

withContext(IO) {
    retrofit.load()
}

线程路径:

text 复制代码
Main
 ↓
IO-1
 ↓
IO-2
 ↓
IO-3

虽然比 launch(IO) 好一点,

但本质还是:

把协程当线程切换框架使用。


第三境:开始定义 suspend

这一阶段开始意识到:

text 复制代码
异步能力应该向下沉。

开始写:

kotlin 复制代码
interface UserRepository {
    suspend fun getUser(): User
}

而不是:

kotlin 复制代码
fun getUser(callback: (User) -> Unit)

或者:

kotlin 复制代码
fun getUser(): Deferred<User>

ViewModel:

kotlin 复制代码
viewModelScope.launch {
    val user = repository.getUser()
}

Repository:

kotlin 复制代码
override suspend fun getUser(): User {
    return api.getUser()
}

此时开始理解:

suspend 描述的是异步能力。

也开始理解:

text 复制代码
同步写法
异步执行

这是协程表达异步的方式。


第四境:开始理解 Main Safe

继续深入以后会发现:

真正负责线程的,

应该是:

text 复制代码
Data Layer

而不是:

text 复制代码
ViewModel

例如:

kotlin 复制代码
override suspend fun getUser(): User {
    return  dao.query()
    
}

于是上层变成:

kotlin 复制代码
viewModelScope.launch {
    val user = repository.getUser()
}

ViewModel 不需要知道:

  • Room
  • Retrofit
  • 文件 IO
  • ContentProvider
  • Binder

是否阻塞。

这就是:

text 复制代码
Main Safe

调用方无需关心线程。

这其实已经进入架构设计阶段了。


第五境:开始理解"不是所有 suspend 都需要 IO"

很多人最终会发现:

下面这些其实根本不需要:

kotlin 复制代码
withContext(IO) {
    retrofitApi.load()
}

因为:

text 复制代码
Retrofit suspend
OkHttp Dispatcher

本身已经异步。


Room:

text 复制代码
Room Query Executor

DataStore:

text 复制代码
DataStore Scope

Coil:

text 复制代码
ImageLoader Dispatcher

很多 SDK:

text 复制代码
自身线程池

这时候再包一层:

kotlin 复制代码
withContext(IO)

往往只是:

text 复制代码
Main
 ↓
IO Dispatcher
 ↓
SDK线程池

多了一层调度中转站。

这也是很多人看到:

text 复制代码
DefaultDispatcher-worker-1
DefaultDispatcher-worker-2
OkHttp Dispatcher
Room Executor

线程数量疯狂增长的原因。


第六境:开始设计执行模型,而不是 Dispatcher

真正高级的协程开发者已经很少讨论:

text 复制代码
这里该不该 IO?

而是在思考:

text 复制代码
谁拥有执行权?

例如:

Application:

kotlin 复制代码
private val appExecutor =
    Executors.newFixedThreadPool(4)

val applicationScope =
    CoroutineScope(
        SupervisorJob() +
        appExecutor.asCoroutineDispatcher()
    )

所有业务任务:

kotlin 复制代码
applicationScope.launch {
}

真正阻塞:

kotlin 复制代码
withContext(ioDispatcher) {
}

整个 App:

  • 最大线程数可控
  • 调度模型可预测
  • 不会出现几十个 worker 线程

这已经开始进入:

text 复制代码
执行模型设计

而不是:

text 复制代码
线程切换技巧

第七境:忘掉 Dispatcher,开始思考边界

最后会发现:

协程真正重要的其实只有三个问题:

谁负责生命周期?

text 复制代码
viewModelScope
applicationScope
lifecycleScope

谁负责取消?

text 复制代码
Job
SupervisorJob

谁负责执行?

text 复制代码
Retrofit
Room
DataStore
AppExecutor

而:

text 复制代码
Dispatchers.IO
Dispatchers.Default

反而越来越少出现。


最终境界

从:

text 复制代码
协程 = 开线程

到:

text 复制代码
协程 = 异步编程

再到:

text 复制代码
协程 = 生命周期管理

最后到:

text 复制代码
协程 = 执行模型设计

这基本就是一个 Android 开发者从初级到高级,再到架构师的协程认知升级路线。

很多人学了几年协程,依然停留在:

text 复制代码
这个接口要不要切 IO?

而真正的问题其实早就变成:

text 复制代码
这个任务到底应该由谁执行?
谁应该拥有调度权?
谁应该拥有生命周期?

当开始问这些问题的时候,说明已经开始真正理解协程了。

参考

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

"结构化"这个词,本质上就是------把混乱的东西变成有组织、有规则、有边界的东西

从"调用方的如履薄冰"到"接口的天然语义":Room/DataStore/Retrofit 的启示

现代 Android 官方为什么更推荐 Repository 暴露 suspend fun,而不是在内部 launch

# Android 架构指南之Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期

别再 launch(IO) 了:协程线程切换的 3隐藏反模式

从送外卖看Android Clean架构:为什么老板不需要知道外卖员开什么车

用一个小 Demo,带你入门安卓 Clean Architecture

Android 现代架构不需要事件总线

为什么我不在 Android ViewModel 中直接处理异常?

相关推荐
-SOLO-11 小时前
Android Event 日志完全指南
android
壮哥_icon12 小时前
【Android 系统开发】从掉帧卡顿到丝滑:高频硬件中断(IRQ)的 CPU 亲和性性能优化实战
android·性能优化
千里马学框架12 小时前
Android Framework 新手学习踩坑建议指南
android·智能手机·性能优化·framework·aaos·车载开发·系统工程师
我命由我1234514 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
狂奔solar15 小时前
ARMA 模型:从白噪声到万能逼近器
android
渣渣灰飞15 小时前
MySQL 系统学习 第五阶段:企业级 MySQL 实战开发 第二章:RBAC 权限系统设计
android·学习·mysql
qq_4228286215 小时前
android 图形学之图层数据送显及合成图层(七)
android
MiyamuraMiyako17 小时前
Compose:从自动滚动到双锚点 LazyLayout
android·android jetpack
用户634214199350718 小时前
base_logger_core --- 跨平台 C++ 日志核心库
android