Android 协程-Dispatchers

4 阅读6分钟

Dispatchers是协程的调度器核心类,作用是指定协程代码运行在哪个线程 / 线程池上,是实现协程线程切换的关键,Android 开发中会结合平台特性有专属的取值和使用规范,下面会把通用取值Android 专属取值都讲清楚,同时区分核心区别、适用场景和使用注意事项。

一、Dispatchers 核心取值(通用 + Android 专属)

Kotlin 协程的Dispatchers提供了5 个核心调度器(其中Main是 Android / 桌面端专属,纯 Kotlin 后端开发无此值),所有调度器本质都是CoroutineDispatcher的实现,底层基于线程池管理,支持协程的轻量切换,核心分为专用调度器可继承调度器两类,先看全量取值和核心说明:

调度器所属类型运行线程 / 线程池核心特点核心适用场景
Dispatchers.Main平台专属Android 主线程 / 桌面 UI 线程单线程、支持 UI 操作Android 更新 UI、轻量 UI 相关逻辑
Dispatchers.IO通用(IO 密集)共享的后台 IO 线程池多线程、适合阻塞 IO网络请求、数据库操作、文件读写
Dispatchers.Default通用(CPU 密集)共享的后台 CPU 线程池多线程、核心数 = CPU 核心数数据解析、排序、计算、循环处理
Dispatchers.Unconfined通用(无限制)先在调用线程执行,后续随挂起恢复无固定线程、非线程池管理纯挂起逻辑、无耗时的协程调度
Dispatchers.Main.immediateAndroid 专属Android 主线程(立即执行)主线程、避免不必要的切换主线程内的挂起后立即执行逻辑

下面逐个拆解每个调度器的细节区别底层实现Android 实际使用规范,这是开发中最核心的部分。


二、逐个详解:Dispatchers 各取值核心区别

1. Dispatchers.Main(Android 专属核心)

核心特性

  • 绑定Android 主线程(UI 线程)单线程执行,和 Activity/Fragment 的 UI 操作在同一线程;
  • 依赖 Android 的主线程消息循环(Looper.getMainLooper ()),协程的代码会被封装成Runnable加入主线程消息队列;
  • 唯一能执行 UI 操作的调度器,在其他调度器中直接更新 UI 会抛出CalledFromWrongThreadException异常。

底层依赖

Android 中使用该调度器,需要引入协程 Android 支持库(否则会报错),gradle 依赖(一般 AndroidX 项目已内置):

gradle

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"

实际使用

Android 中lifecycleScope/viewModelScope 的默认调度器就是 Main,因此直接启动的协程可安全更新 UI,无需手动指定:

kotlin

// 默认为Dispatchers.Main,可直接更新UI
lifecycleScope.launch {
    textView.text = "更新UI" // 安全
}

2. Dispatchers.IO(最常用的后台调度器)

核心特性

  • 专为IO 密集型任务设计,底层是共享的后台线程池,线程数无固定上限(可根据系统资源动态调整);
  • 支持阻塞式操作(如网络请求的okhttp、数据库的Room、文件读写),协程挂起时会释放线程,避免线程阻塞浪费资源;
  • Dispatchers.Default共享线程池资源,底层会根据任务类型动态调整线程,因此切换两者无额外性能开销。

关键注意

  • 不要在 IO 调度器中执行CPU 密集型任务(如大集合排序、复杂计算),否则会占用 IO 线程池资源,导致网络 / 文件操作阻塞;
  • Room、Retrofit 等主流库已支持挂起函数,内部会自动切换到 IO 线程,因此调用时无需手动指定Dispatchers.IO(避免重复切换)。

实际使用

通过withContext切换到 IO 线程执行耗时操作,执行完成后会自动切回原调度器(如 Main),无需手动处理线程切换:

kotlin

lifecycleScope.launch { // 原调度器Main
    // 切换到IO执行耗时操作,执行完自动切回Main
    val data = withContext(Dispatchers.IO) {
        api.fetchData() // 网络请求(IO密集)
    }
    textView.text = data // 切回Main,安全更新UI
}

3. Dispatchers.Default(CPU 密集型任务专属)

核心特性

  • 专为CPU 密集型任务设计,底层是共享的后台线程池核心线程数 = CPU 核心数(如手机 8 核则核心数为 8);
  • 多线程并行执行,能最大化利用 CPU 资源,适合计算密集型操作;
  • 线程池为懒加载,只有当有任务执行时才会创建线程,避免空闲时占用系统资源。

关键注意

  • 核心线程数固定,不要在 Default 中执行阻塞式 IO 操作,否则会导致线程池被阻塞,其他 CPU 密集型任务无法执行;
  • 若 CPU 密集型任务耗时过长(如超过 1 秒),建议拆分任务,避免占用核心线程导致 UI 卡顿(间接影响主线程)。

实际使用

同样通过withContext切换,适合数据解析、排序、加密解密等场景:

kotlin

lifecycleScope.launch {
    // 切换到Default处理CPU密集任务
    val sortedList = withContext(Dispatchers.Default) {
        bigList.sortedBy { it.id } // 大集合排序(CPU密集)
    }
    // 切回Main更新UI
    recyclerView.adapter.submitList(sortedList)
}

4. Dispatchers.Unconfined(无限制调度器,慎用)

核心特性

  • 「无限制」指不绑定任何固定线程 / 线程池,协程的执行线程随挂起点动态变化

    1. 协程启动时,在调用线程执行(如主线程调用,则先在主线程跑);
    2. 当遇到挂起函数(如delaywithContext)时,协程会挂起,挂起恢复后,在挂起函数的执行线程继续运行
  • 不占用线程池资源,适合纯挂起逻辑(无实际耗时的 CPU/IO 操作)。

核心问题(慎用原因)

  • 线程不可控:恢复后的执行线程无法预测,若恢复后在后台线程执行 UI 操作,会直接抛出异常;
  • 可能导致主线程阻塞:若协程中无任何挂起函数,会一直在调用线程(如主线程)执行,若包含耗时操作,会直接阻塞主线程导致 UI 卡顿。

唯一合理使用场景

仅适用于无耗时操作、仅包含挂起函数的轻量协程,比如:

kotlin

// 仅挂起,无耗时操作,可使用Unconfined
launch(Dispatchers.Unconfined) {
    delay(1000) // 挂起,恢复后线程由delay内部决定
    log("执行:${Thread.currentThread().name}")
}

Android 开发中几乎不用,新手直接忽略即可。

5. Dispatchers.Main.immediate(Android 专属,Main 的增强版)

核心特性

  • Dispatchers.Main立即执行版本,同样运行在Android 主线程
  • Main的核心区别:若当前已经在主线程,会立即执行协程代码,不会加入消息队列;若当前在后台线程,则和Main一致,加入主线程消息队列。
  • Main的逻辑:无论当前是否在主线程,都会将代码封装成Runnable加入主线程消息队列,等待 Looper 调度执行(有延迟)。

适用场景

适合在主线程中调用、需要立即执行的协程逻辑,避免不必要的消息队列调度延迟,比如:

kotlin

// 主线程中执行以下代码
lifecycleScope.launch(Dispatchers.Main) {
    println("Main:${System.currentTimeMillis()}") // 加入消息队列,延迟执行
}

lifecycleScope.launch(Dispatchers.Main.immediate) {
    println("Main.immediate:${System.currentTimeMillis()}") // 立即执行,无延迟
}

输出顺序:Main.immediate 先执行,Main 后执行。

日常开发中无需刻意指定,仅在需要「主线程立即执行」的特殊场景使用。


三、Dispatchers 核心通用规则(Android 开发必守)

这部分是避免协程线程问题的关键,比记住调度器本身更重要:

1. 线程切换核心:withContext

协程中唯一推荐的线程切换方式withContext,而非直接在launch/async中指定调度器,原因:

  • withContext执行完成后,会自动切回外层协程的调度器(如从 IO 切回 Main),无需手动通过Handler/runOnUiThread切换;
  • 代码更简洁,避免嵌套的线程切换逻辑;
  • withContext自动处理异常,并将异常抛给外层协程,便于统一捕获。

反例(不推荐) :直接指定调度器,更新 UI 需要手动切回 Main

kotlin

// 不推荐:需要手动处理线程切换,代码繁琐
lifecycleScope.launch(Dispatchers.IO) {
    val data = api.fetchData()
    // 手动切回Main更新UI,麻烦且易忘
    withContext(Dispatchers.Main) {
        textView.text = data
    }
}

正例(推荐) :外层默认 Main,内层 withContext 切换后台

kotlin

// 推荐:自动切回Main,代码简洁
lifecycleScope.launch {
    val data = withContext(Dispatchers.IO) { api.fetchData() }
    textView.text = data
}