在 Kotlin 协程中,有一个非常隐蔽但真实存在的问题:
你只是加了
Dispatchers.IO,线程数却变多了,多的不是一点点,而是成倍增长
更关键的是:
👉 这个问题在 Application 启动阶段,比 ViewModel 更严重
0. 先讲清楚“业务真实场景”
为了让问题更贴近真实工程,我们假设一个典型 App:
App 启动 & UI 初始化行为
整个应用在启动时,会同时触发两类并发任务:
① Application 启动阶段(全局初始化)
1启动 App → 自动触发 4 个并发初始化任务 2
例如:
- 数据库初始化
- SDK 初始化
- 配置加载
- 日志系统初始化
② UI 页面启动阶段(业务并发)
1进入首页 → ViewModel 自动触发 4 个并发请求 2
例如:
- 用户信息
- 首页列表
- 推荐流
- 未读消息
👉 两个阶段一共:8 个并发任务
我们用统一任务模型模拟
为了简化问题,我们定义一个任务执行器:
1object TaskRunner { 2 3 private val executor = Executors.newFixedThreadPool(10) { 4 Thread(it, "TaskRunner-Pool") 5 } 6 7 suspend fun test(time: Long): String = 8 withContext(executor.asCoroutineDispatcher()) { 9 Thread.sleep(time) // 模拟真实 IO 10 "done on ${Thread.currentThread().name}" 11 } 12} 13
关键点
✔ TaskRunner 已经自带线程池 ✔ 已经是完整执行模型 ✔ 已经“会自己跑”
这是模拟 网络请求的耗时任务,比如Retrofit.
1. ViewModel 场景:UI 并发请求
首页 ViewModel:
1viewModelScope.launch { 2 launch(Dispatchers.IO) { 3 repeat(4) { 4 launch { 5 TaskRunner.test(1000) 6 } 7 } 8 } 9} 10
线程现象
1DefaultDispatcher-worker-1 | TIMED_WAITING 2DefaultDispatcher-worker-2 | TIMED_WAITING 3DefaultDispatcher-worker-3 | TIMED_WAITING 4DefaultDispatcher-worker-4 | TIMED_WAITING 5 6TaskRunner-Pool-1 | WAITING 7TaskRunner-Pool-2 | WAITING 8
🔍 执行链路
1Main 2 → Dispatchers.IO(调度层) 3 → Default Dispatcher(中转层) 4 → TaskRunner Pool(真正执行) 5
问题本质
IO 在这里没有做任何业务执行,只做了三件事:
- 转发任务
- 唤醒 worker
- 切换上下文
👉 但真正干活的是 TaskRunner
2. Application 场景:问题开始放大(关键)
App 启动时:
1class App : Application() { 2 val appScope = CoroutineScope(SupervisorJob()) 3 override fun onCreate() { 4 super.onCreate() 5 appScope.launch(Dispatchers.IO) { 6 repeat(4) { 7 launch { 8 TaskRunner.test(1000) 9 } 10 } 11 } 12 } 13} 14
Application 阶段发生了什么?
启动 App 的瞬间:
1系统自动触发: 2→ 初始化 4 个并发任务 3
Application 线程现象
1DefaultDispatcher-worker-1 | TIMED_WAITING 2DefaultDispatcher-worker-2 | TIMED_WAITING 3DefaultDispatcher-worker-3 | TIMED_WAITING 4DefaultDispatcher-worker-4 | TIMED_WAITING 5 6TaskRunner-Pool-1 | WAITING 7TaskRunner-Pool-2 | WAITING 8
3. 关键差异:为什么 Application 更“炸”?
✔ 特点 1:没有 UI 节奏控制
ViewModel:
- 页面触发
- 用户行为驱动
- 有生命周期 cancel
Application:
- 一次性启动
- 无节流
- 全局 Scope
👉 **所有任务瞬间发射 **
✔ 特点 2:初始化任务天然 IO 密集
典型 App 初始化:
- DB init
- SDK init
- config load
- log system
- network
开发者第一反应:
1Dispatchers.IO 2
✔ 特点 3:任务嵌套更深
1launch(Dispatchers.IO) { 2 initA() 3 initB() 4 initC() 5} 6
每个 init 可能还会:
- 再 launch(IO)
- 再 withContext(IO)
- 再 collect Flow
👉 Application 直接形成:
1IO → IO → IO → Executor 2
4. 核心问题:调度冗余
真正问题不是线程多,而是:
❗ 同一个任务被多个 Dispatcher 重复“搬运”
❌ 错误认知
1Dispatchers.IO = 提升性能 2
❌ 实际发生
1launch(IO) 2 → launch(Default) 3 → withContext(executor) 4
👉 结果:
- 3 层调度
- 多次线程切换
- worker 空转
5. 最关键现象:大量“不会干活的线程”
你会看到:
1DefaultDispatcher-worker-1 | TIMED_WAITING 2DefaultDispatcher-worker-2 | TIMED_WAITING 3DefaultDispatcher-worker-3 | TIMED_WAITING 4
❗ 这些线程在干嘛?
答案是:
👉 在“搬运任务”,而不是执行任务
6. ViewModel vs Application 对比
| 维度 | ViewModel | Application |
|---|---|---|
| 生命周期 | 短 | 长 |
| 并发方式 | 分散触发 | 一次爆发 |
| IO 使用 | 局部 | 全局 |
| 控制能力 | cancel | 无 cancel |
| 线程风险 | 中 | 高 |
👉 Application 本质:
没有节奏,只有洪峰
7. 🔥 本质总结
真正的问题不是:
❌ 并发太多
而是:
❗ 你让一个已经具备执行能力的系统,被 Dispatchers.IO 再调度了一次
❌ 典型反模式
1launch(Dispatchers.IO) { 2 TaskRunner.test() 3} 4
但 TaskRunner 本身已经:
1withContext(executor) 2
👉 等价于:
IO 再调度 IO 已经调度好的任务
8. ✔ 正确原则
✔ 原则 1:Application 不要统一 IO 化
1appScope.launch { 2 TaskRunner.test() 3} 4
✔ 原则 2:IO 只用于“阻塞点”
1withContext(Dispatchers.IO) { 2 File.read() 3} 4
✔ 原则 3:避免 IO 套 IO
1❌ IO → IO → Executor 2
✔ 原则 4:执行模型应该下沉到 Data 层
1suspend fun test() = 2 withContext(executor) { } 3
9. 结论
👉 当底层已经是“异步执行模型”时,上层scope的线程调度应该被束缚在一个小范围
源码:ScopeSample
这个是源码里的场景
这是去除在viewModel 里去除
1launch(Dispatchers.IO) 2
和 Application 里换成
1private val applicationScope = CoroutineScope(SupervisorJob() + appExecutor.asCoroutineDispatcher()) 2
的场景