1) Dispatcher 是什么
CoroutineDispatcher 实质是 ContinuationInterceptor:
-
决定协程在哪个线程(池)执行与恢复;
-
调度策略(立即/投递、队列、合并等);
-
与挂起函数配合:挂起点之后的恢复也受它控制。
常见切换方式:launch(context) / async(context) / withContext(context);Flow 用 flowOn(context) 切上游。
2) 内置调度器与适用场景
Dispatchers.Main(Android)
-
绑定 主线程 Looper(来自 kotlinx-coroutines-android)。
-
用于 UI 更新、生命周期回调、轻量逻辑。
-
Main.immediate:若已在主线程,直接(不经消息队列)执行,减少一次切换;否则还是投递到主线程。
-
典型用法:viewModelScope.launch(Dispatchers.Main.immediate) { … },避免在快速连续回调里反复切主线程导致闪动/抖动。
-
Dispatchers.Default(CPU 密集)
-
面向 CPU 计算(解析、排序、diff、压缩、加密、JSON 转换…)。
-
线程数≈CPU 核心数(至少 2),共享池,公平调度。
-
不要做阻塞 I/O(会占满计算位点);如需阻塞,改用 IO。
Dispatchers.IO(阻塞 I/O)
-
目标:文件/网络/数据库等“可能阻塞线程”的操作。
-
与 Default 共享底层池,但 允许更高并行度 以“吸收”阻塞任务(避免拖慢 CPU 计算)。
-
若你调用的是真正挂起的非阻塞 I/O(如 Retrofit + 挂起),放 IO 或 Default 都行;但一旦可能阻塞(老库、JNI、系统调用)就放 IO。
Dispatchers.Unconfined(不限定)
- 启动时在当前线程执行,首次挂起后由挂起函数的恢复线程决定去哪。
- 易造成线程漂移与不可预测时序,不要用于 UI/生产代码;仅用于特定测试或底层框架代码。
3) Android 细节与常用搭配
- lifecycleScope.launch(Main):启动 UI 协程;耗时用 withContext(Default/IO) 切出去再回来。
- viewModelScope 默认在 Main;在 VM 内进行短任务:withContext(Default) 做计算;withContext(IO) 读写本地/网络。
- 避免在 Main 中阻塞(如 Thread.sleep / 同步 I/O / 大循环),否则掉帧和 ANR。
- Main.immediate 用于去抖/UI原地更新,例如快速合流的 StateFlow 收集场景可减少一次切换。
4) 上下文切换语义
- withContext(X):在同一协程内切到 X,并在块结束后切回原 Dispatcher。
- launch/async(X):新协程继承父上下文,除非你显式传入 X(覆盖)。
- 嵌套切换可读性差且有开销;把“在哪里执行”的决策放在边界层(仓库/UseCase/数据源)。
5) 并发控制 & 自定义
limitedParallelism
- 给任何 dispatcher 限流:
val singleIO = Dispatchers.IO.limitedParallelism(1) // 串行化一个资源
withContext(singleIO) { writeFileAThenB() }
-
比 newSingleThreadContext 更推荐(后者容易泄漏,需手动 close)。
与 Executor 互转
val dispatcher = myExecutor.asCoroutineDispatcher()
// or
val dispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
仍建议优先用 Default/IO + limitedParallelism,除非你需要绑定特定线程或已有线程池生态。
6) Flow 与 Dispatcher
- flowOn(context):改变上游(生产/变换)的执行上下文;下游收集仍在调用者的上下文。
flow { emit(loadDb()) } // 默认跟随 collector
.map { heavyCompute(it) } // 也会被切到 Default
.flowOn(Dispatchers.Default) // 上游生产+map 在 Default
.collect { renderOnUi(it) } // 收集仍在 Main
-
launchIn(scope):收集发生在 scope 的上下文。
-
高频 UI 列表:在 ViewModel 用 flowOn(Default) 做变换、buffer() 平滑背压,UI 层 collectLatest。
7) 性能与正确性常见坑
- 在 Main 做阻塞 → 掉帧/ANR。用 withContext(IO/Default)。
- 在 Default 做阻塞 I/O → 抢占 CPU 池,拖慢计算任务。用 IO。
- 滥用 Unconfined → 非确定性线程切换/竞态。
- 创建过多专用线程(newSingleThreadContext)→ 资源泄漏。改 limitedParallelism(1)。
- 过度切换(层层 withContext)→ 可读性差 + 调度开销。把切换收敛到边界。
- 误解 Flow 线程:flowOn 只影响上游;UI 更新仍需在 Main。
- 阻塞式同步库放 Default:应包一层 withContext(IO);若库可回调/异步,优先改造为挂起。
8) 调试与测试
- 线程名带协程调试信息:JVM 启动参数 -Dkotlinx.coroutines.debug(线程名会附 @coroutine#id)。
- 单元测试替 Main:Dispatchers.setMain(TestDispatcher),结束后 Dispatchers.resetMain()。
- 观察切换:在关键 withContext 前后打点;或用 DebugProbes(调试构建)。
9) 选型速查(Cheat Sheet)
- UI 更新/收集状态 → Main(必要时 Main.immediate)。
- CPU 密集(解析、Diff、加解密、图片处理)→ Default。
- 阻塞 I/O(老式文件/Socket/DB 驱动、JNI 调用)→ IO。
- 串行访问某资源(日志文件/同一表)→ Dispatchers.IO.limitedParallelism(1)。
- 避免 → Unconfined(除非你清楚恢复线程语义)。
10) 代码模板(Android ViewModel)
class ProfileVM : ViewModel() {
val ui = MutableStateFlow<UiState>(UiState.Loading)
fun load() = viewModelScope.launch(Dispatchers.Main) {
val user = withContext(Dispatchers.IO) { dao.queryUser() } // 阻塞 I/O
val diff = withContext(Dispatchers.Default) { computeDiff(user) } // CPU 计算
ui.value = UiState.Ready(diff)
}
}