在 Kotlin 协程开发中,这种代码几乎随处可见:
kotlin
viewModelScope.launch(Dispatchers.IO) {
val user = repository.getUser()
}
很多开发者已经习惯了这种写法。
需要执行网络请求?
kotlin
launch(Dispatchers.IO)
需要访问数据库?
kotlin
launch(Dispatchers.IO)
需要调用 Repository?
kotlin
launch(Dispatchers.IO)
久而久之,launch(Dispatchers.IO) 似乎变成了协程代码中的"标准模板"。
但随着对协程和异步执行模型理解的深入,我想说:
大部分
launch并不需要Dispatchers.IO。
因为:
launch的核心价值不是"切线程",而是"创建异步任务"。
而数据层应该自己负责它所依赖的底层执行模型。
一、我们是不是误解了 launch?
先看一个最简单的例子:
kotlin
viewModelScope.launch {
val user = repository.getUser()
updateUi(user)
}
很多人第一反应是:
这里没有
Dispatchers.IO,那网络请求不是跑在 Main 线程了吗?
这个问题本身其实就暴露了一个误区。
我们把:
kotlin
launch
和:
kotlin
线程切换
绑定得太紧了。
实际上,launch 的核心语义是:
创建一个新的子协程,也就是一个新的异步任务。
例如:
kotlin
coroutineScope {
launch {
loadUser()
}
launch {
loadConfig()
}
}
这里真正重要的是:
text
coroutineScope
/ \
/ \
launch A launch B
│ │
loadUser() loadConfig()
\ /
\ /
全部完成
我们使用 launch,是因为我们需要两个并发任务。
这里的重点是:
并发。
所以更准确的理解应该是:
text
launch
↓
创建新的 Coroutine
↓
创建新的 Job
↓
参与结构化并发
↓
由 Dispatcher 决定如何调度
因此:
kotlin
launch(Dispatchers.IO)
并不是:
"launch 就是切换到 IO 线程。"
而应该理解成:
创建一个新的并发任务,并指定它使用
Dispatchers.IO进行调度。
这是两个不同的概念。
二、launch 和 withContext 解决的问题并不一样
如果我们只是需要把一段代码放到某个 Context 中执行:
kotlin
withContext(Dispatchers.IO) {
blockingOperation()
}
它和:
kotlin
launch(Dispatchers.IO) {
blockingOperation()
}
表达的意图完全不同。
withContext 更接近:
当前任务,暂时使用另一个 Context 执行这段代码。
而 launch 更接近:
创建一个新的子任务。
可以简单理解为:
text
withContext
↓
我还是当前这个任务
↓
只是换一个 Context 执行
而:
text
launch
↓
我要创建一个新的任务
↓
让它参与并发执行
所以:
kotlin
val user = withContext(Dispatchers.IO) {
database.queryUser()
}
表达的是:
我需要执行一个 IO 操作,执行完以后继续当前流程。
而:
kotlin
launch(Dispatchers.IO) {
database.queryUser()
}
表达的是:
我现在要创建一个新的并发任务。
这两个需求本身就不一样。
因此:
不要把
launch当成线程切换 API。
三、为什么我越来越少写 launch(Dispatchers.IO)?
因为现代 Android 的 Data 层,很多 API 本身已经具备自己的异步执行模型。
例如 Retrofit:
kotlin
interface UserApi {
@GET("user")
suspend fun getUser(): User
}
Repository:
kotlin
class UserRepository(
private val api: UserApi
) {
suspend fun getUser(): User {
return api.getUser()
}
}
ViewModel:
kotlin
viewModelScope.launch {
val user = repository.getUser()
updateUi(user)
}
这里:
kotlin
viewModelScope.launch
负责的是:
启动一个生命周期属于 ViewModel 的协程任务。
而:
kotlin
repository.getUser()
负责的是:
获取数据。
真正的网络请求执行模型,由 Retrofit/OkHttp 这一层负责。
因此,我们没有必要机械地写:
kotlin
viewModelScope.launch(Dispatchers.IO) {
repository.getUser()
}
因为:
ViewModel 并不拥有网络请求的执行模型。
它只是在发起一个任务。
四、Data 层应该成为执行模型的拥有者
这也是我认为非常重要的一个架构原则:
谁拥有执行模型,谁负责调度。
例如 Data 层使用的是异步 API:
kotlin
suspend fun getUser(): User {
return api.getUser()
}
那么:
text
ViewModel
↓
launch
↓
Repository
↓
Retrofit suspend API
↓
底层异步执行
上层不需要额外添加:
kotlin
Dispatchers.IO
因为底层已经有自己的执行模型。
但是,如果 Data 层使用的是阻塞 API:
kotlin
fun queryUser(): User {
return blockingDatabase.query()
}
那么 Data 层就应该负责隔离阻塞操作:
kotlin
suspend fun queryUser(): User {
return withContext(Dispatchers.IO) {
blockingDatabase.query()
}
}
这样上层依然可以:
kotlin
viewModelScope.launch {
val user = repository.queryUser()
}
整个调用链就变成:
text
ViewModel
│
│ launch
│
▼
Repository
│
│ 发现底层是 Blocking API
│
▼
withContext(IO)
│
▼
Blocking IO
这里就形成了非常清晰的职责边界:
ViewModel 负责启动任务。
Data 层负责保证 API 的 Main-safe。
底层执行模型决定是否真的需要 Dispatcher。
五、Data 层应该保证 Main-safe
Data 层应该做到的是:
调用者不需要知道 Data 层内部是如何执行的。
例如:
kotlin
viewModelScope.launch {
repository.getUser()
}
无论这个 launch 默认运行在哪里,调用都不能导致:
text
Main Thread
↓
Repository
↓
Blocking IO
↓
ANR
因此,Data 层的契约应该是:
即使调用者在 Main 上调用,我也不会阻塞 Main。
这就是 Main-safe。
例如:
kotlin
class UserRepository {
suspend fun getUser(): User {
return withContext(Dispatchers.IO) {
blockingApi.getUser()
}
}
}
那么调用者完全可以:
kotlin
viewModelScope.launch {
repository.getUser()
}
而不需要关心内部到底使用了:
kotlin
Dispatchers.IO
还是:
kotlin
Dispatchers.Default
甚至未来底层 API 改成真正的异步 API:
kotlin
suspend fun getUser(): User {
return asyncApi.getUser()
}
上层代码也不需要改变。
这就是职责的隔离。
六、如果底层已经异步,再套一层 IO,只是调度冗余
假设底层 API 已经是异步的:
kotlin
suspend fun getUser(): User
上层:
kotlin
viewModelScope.launch(Dispatchers.IO) {
repository.getUser()
}
很多时候,这个 Dispatchers.IO 并没有解决真正的问题。
因为:
text
ViewModel
↓
launch(IO)
↓
Repository
↓
Suspend API
↓
底层异步执行模型
此时就出现一种"调度冗余":
text
上层调度一次
↓
底层自己调度
↓
实际执行
上层只是为了"保险"加了一层 IO。
但实际上:
它只是 Kotlin 协程生态中的一种 Dispatcher。
真正需要关注的问题应该是:
底层 API 到底是不是阻塞的?
如果是阻塞的:
text
Blocking
↓
需要隔离
↓
可能使用 IO
如果本身已经是异步挂起:
text
Suspend / Async
↓
已经有执行模型
↓
通常不需要上层额外切 IO
所以,与其问:
"这个请求要不要使用
Dispatchers.IO?"
不如先问:
"这个 API 的执行模型是什么?"
七、不要让 launch(IO) 变成条件反射
下面这种代码:
kotlin
viewModelScope.launch(Dispatchers.IO) {
repository.getUser()
}
应该让我们多问一个问题:
为什么这里必须是 IO?
而不是:
为什么这里没有 IO?
如果答案只是:
"因为网络请求。"
那么可能需要重新思考。
如果答案是:
"因为 Repository 内部调用了阻塞 API。"
那么这个 IO 是否应该由 Repository 内部负责?
如果答案是:
"因为数据库操作。"
那么这个数据库 API 是阻塞的,还是已经是 suspend / Flow?
如果答案是:
"因为大家都这么写。"
那么可能就应该删除它。
协程代码真正成熟的标志之一,就是:
开始减少那些没有明确理由的 Dispatcher。
八、重新理解 Coroutine 的三个核心角色
我认为可以把协程中的三个概念简单拆开:
text
CoroutineScope
↓
生命周期边界
launch
↓
创建并发任务
Dispatcher
↓
调度策略
因此:
kotlin
viewModelScope.launch {
repository.getUser()
}
表达的是:
text
ViewModel 生命周期
↓
创建一个任务
↓
调用 Repository
而:
kotlin
withContext(Dispatchers.IO) {
blockingOperation()
}
表达的是:
text
当前任务
↓
执行一段阻塞操作
↓
需要使用 IO 调度策略
如果把所有东西都写成:
kotlin
launch(Dispatchers.IO)
其实是把:
text
生命周期
并发
调度
三个概念全部揉在一起了。
最终代码看起来虽然简单,但职责边界反而变得模糊。
九、我现在更推荐的思考方式
我不会再看到:
kotlin
launch {
}
第一反应就是:
"这里是不是漏了
Dispatchers.IO?"
我会先问三个问题:
第一:我需要并发吗?
如果需要:
kotlin
launch
如果不需要,只是希望改变执行 Context:
kotlin
withContext
第二:底层 API 是阻塞的吗?
如果是:
kotlin
withContext(Dispatchers.IO)
或者由更靠近阻塞源的 Data 层负责。
如果本身已经是:
kotlin
suspend
或者:
kotlin
Flow
并且底层已经有合理的异步执行模型,那么通常不需要再机械地套一层 IO。
第三:谁应该负责调度?
我的答案是:
谁拥有执行模型,谁负责调度。
ViewModel 不应该默认决定整个调用链使用什么 Dispatcher。
Data 层应该保证 Main-safe。
底层库应该负责自己的执行模型。
上层只需要表达业务并发关系。
总结
协程真正解决的问题,从来不只是"线程切换"。
如果把协程理解成:
text
Thread
↓
换成
Coroutine
那么我们最终还是会陷入:
kotlin
launch(Dispatchers.IO)
launch(Dispatchers.Default)
withContext(Dispatchers.IO)
不断切换 Dispatcher 的思维。
但如果从结构化并发的角度看,协程的核心其实是:
text
Scope
↓
管理生命周期
launch
↓
创建并发任务
withContext
↓
改变执行 Context
Dispatcher
↓
提供调度策略
Job
↓
管理任务关系和取消
所以:
launch的价值不是"切线程",而是"创建并发任务"。
withContext的价值不是"开启异步",而是"在另一个 Context 中执行代码"。
Dispatchers.IO不是协程的"异步开关",而是一种调度策略。
而在现代 Android 应用中,如果 Data 层使用的底层 API 本身已经是异步的,那么 ViewModel 层大多数情况下完全可以:
kotlin
viewModelScope.launch {
repository.getUser()
}
把:
text
并发
交给 launch,
把:
text
执行模型
交给 Data 层和底层库,
把:
text
调度策略
交给真正拥有执行模型的那一层。
我们最终会发现:
好的协程代码,不应该到处写
Dispatchers.IO,而是让每一层只负责自己真正应该负责的事情。