协程深度解析:明明只有 8 个异步任务,为何 App 线程数瞬间突破 一倍?

在 Kotlin 协程中,有一个非常隐蔽但真实存在的问题:

你只是加了 Dispatchers.IO,线程数却变多了,多的不是一点点,而是成倍增长

更关键的是:

👉 这个问题在 Application 启动阶段,比 ViewModel 更严重


0. 先讲清楚"业务真实场景"

为了让问题更贴近真实工程,我们假设一个典型 App:


App 启动 & UI 初始化行为

整个应用在启动时,会同时触发两类并发任务:

① Application 启动阶段(全局初始化)

text 复制代码
启动 App → 自动触发 4 个并发初始化任务

例如:

  • 数据库初始化
  • SDK 初始化
  • 配置加载
  • 日志系统初始化

② UI 页面启动阶段(业务并发)

text 复制代码
进入首页 → ViewModel 自动触发 4 个并发请求

例如:

  • 用户信息
  • 首页列表
  • 推荐流
  • 未读消息

👉 两个阶段一共:8 个并发任务


我们用统一任务模型模拟

为了简化问题,我们定义一个任务执行器:

kotlin 复制代码
object TaskRunner {

    private val executor = Executors.newFixedThreadPool(10) {
        Thread(it, "TaskRunner-Pool")
    }

    suspend fun test(time: Long): String =
        withContext(executor.asCoroutineDispatcher()) {
            Thread.sleep(time) // 模拟真实 IO
            "done on ${Thread.currentThread().name}"
        }
}

关键点

✔ TaskRunner 已经自带线程池 ✔ 已经是完整执行模型 ✔ 已经"会自己跑"

这是模拟 网络请求的耗时任务,比如Retrofit.

1. ViewModel 场景:UI 并发请求

首页 ViewModel:

kotlin 复制代码
viewModelScope.launch {
    launch(Dispatchers.IO) {
        repeat(4) {
            launch {
                TaskRunner.test(1000)
            }
        }
    }
}

线程现象

text 复制代码
DefaultDispatcher-worker-1 | TIMED_WAITING
DefaultDispatcher-worker-2 | TIMED_WAITING
DefaultDispatcher-worker-3 | TIMED_WAITING
DefaultDispatcher-worker-4 | TIMED_WAITING

TaskRunner-Pool-1           | WAITING
TaskRunner-Pool-2           | WAITING

🔍 执行链路

text 复制代码
Main
 → Dispatchers.IO(调度层)
   → Default Dispatcher(中转层)
     → TaskRunner Pool(真正执行)

问题本质

IO 在这里没有做任何业务执行,只做了三件事:

  • 转发任务
  • 唤醒 worker
  • 切换上下文

👉 但真正干活的是 TaskRunner


2. Application 场景:问题开始放大(关键)

App 启动时:

kotlin 复制代码
class App : Application() {
    val appScope = CoroutineScope(SupervisorJob())
    override fun onCreate() {
        super.onCreate()
        appScope.launch(Dispatchers.IO) {
            repeat(4) {
                launch {
                    TaskRunner.test(1000)
                }
            }
        }
    }
}

Application 阶段发生了什么?

启动 App 的瞬间:

text 复制代码
系统自动触发:
→ 初始化 4 个并发任务

Application 线程现象

text 复制代码
DefaultDispatcher-worker-1 | TIMED_WAITING
DefaultDispatcher-worker-2 | TIMED_WAITING
DefaultDispatcher-worker-3 | TIMED_WAITING
DefaultDispatcher-worker-4 | TIMED_WAITING

TaskRunner-Pool-1           | WAITING
TaskRunner-Pool-2           | WAITING

3. 关键差异:为什么 Application 更"炸"?


✔ 特点 1:没有 UI 节奏控制

ViewModel:

  • 页面触发
  • 用户行为驱动
  • 有生命周期 cancel

Application:

  • 一次性启动
  • 无节流
  • 全局 Scope

👉 **所有任务瞬间发射 **


✔ 特点 2:初始化任务天然 IO 密集

典型 App 初始化:

  • DB init
  • SDK init
  • config load
  • log system
  • network

开发者第一反应:

kotlin 复制代码
Dispatchers.IO

✔ 特点 3:任务嵌套更深

kotlin 复制代码
launch(Dispatchers.IO) {
    initA()
    initB()
    initC()
}

每个 init 可能还会:

  • 再 launch(IO)
  • 再 withContext(IO)
  • 再 collect Flow

👉 Application 直接形成:

text 复制代码
IO → IO → IO → Executor

4. 核心问题:调度冗余

真正问题不是线程多,而是:

❗ 同一个任务被多个 Dispatcher 重复"搬运"


❌ 错误认知

text 复制代码
Dispatchers.IO = 提升性能

❌ 实际发生

text 复制代码
launch(IO)
 → launch(Default)
   → withContext(executor)

👉 结果:

  • 3 层调度
  • 多次线程切换
  • worker 空转

5. 最关键现象:大量"不会干活的线程"

你会看到:

text 复制代码
DefaultDispatcher-worker-1 | TIMED_WAITING
DefaultDispatcher-worker-2 | TIMED_WAITING
DefaultDispatcher-worker-3 | TIMED_WAITING

❗ 这些线程在干嘛?

答案是:

👉 在"搬运任务",而不是执行任务


6. ViewModel vs Application 对比

维度 ViewModel Application
生命周期 短 长
并发方式 分散触发 一次爆发
IO 使用 局部 全局
控制能力 cancel 无 cancel
线程风险 中 高

👉 Application 本质:

没有节奏,只有洪峰


7. 🔥 本质总结

真正的问题不是:

❌ 并发太多

而是:

❗ 你让一个已经具备执行能力的系统,被 Dispatchers.IO 再调度了一次


❌ 典型反模式

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

但 TaskRunner 本身已经:

kotlin 复制代码
withContext(executor)

👉 等价于:

IO 再调度 IO 已经调度好的任务


8. ✔ 正确原则


✔ 原则 1:Application 不要统一 IO 化

kotlin 复制代码
appScope.launch {
    TaskRunner.test()
}

✔ 原则 2:IO 只用于"阻塞点"

kotlin 复制代码
withContext(Dispatchers.IO) {
    File.read()
}

✔ 原则 3:避免 IO 套 IO

text 复制代码
❌ IO → IO → Executor

✔ 原则 4:执行模型应该下沉到 Data 层

kotlin 复制代码
suspend fun test() =
    withContext(executor) { }

9. 结论

👉 当底层已经是"异步执行模型"时,上层scope的线程调度应该被束缚在一个小范围

源码: ScopeSample

这个是源码里的场景

这是去除在viewModel 里去除

scss 复制代码
launch(Dispatchers.IO)

和 Application 里换成

scss 复制代码
private val applicationScope = CoroutineScope(SupervisorJob() + appExecutor.asCoroutineDispatcher())

的场景

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui