launch 不是用来切线程的:为什么我越来越少在它后面写 Dispatchers.IO

在 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,而是让每一层只负责自己真正应该负责的事情。

相关推荐
传奇开心果编程2 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓4 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1235 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)5 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
五彩小白1 天前
自回归和上下文
android
事圆则缓1 天前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android