第3章 协程调度器理解:Dispatchers.IO、Dispatchers.Default、Dispatchers.Main 区别与使用场景
本章核心:协程调度器决定了代码在哪个线程池执行。在 Room + Flow 组合中,错误选择调度器将导致主线程 ANR 或 IO 线程堵塞。掌握三个内置调度器的差异,是构建流畅数据层的基石。
3.1 调度器概览
Kotlin 协程提供三个内置调度器:
- Dispatchers.Main – 绑定主线程,用于 UI 更新、事件分发、LiveData 变更。
- Dispatchers.IO – 专为 IO 密集型任务设计(文件、网络、数据库),线程池可动态扩容。
- Dispatchers.Default – 用于 CPU 密集型任务,线程数等于核心数。
| 调度器 | 线程池特性 | 典型任务 |
|---|---|---|
| Dispatchers.Main | 单线程(Android 主线程) | UI 更新、点击回调、Lifecycle 方法 |
| Dispatchers.IO | 动态线程池(上限 64 或 2×核心数) | Room 数据库操作、网络请求、文件读写 |
| Dispatchers.Default | 固定线程池(核心数,至少 2) | 列表排序、JSON 解析、图像压缩 |
3.2 原理与协程调度机制
当使用 flow { ... } 时,发射端和收集端默认运行在调用协程的协程上下文。Room 的 DAO 方法返回的 Flow 在内部已使用 Dispatchers.IO 执行查询,但收集端(collect)默认继承调用者的调度器。若不主动切换,UI 层的 collect 会阻塞主线程。
// 错误示例:collect 时未切换调度器
lifecycleScope.launch {
userDao.getAllUsers().collect { userList ->
// 主线程直接执行阻塞性数据库查询?实际 Room 将查询放到 IO 线程,
// 但 collect 回调本身仍在主线程,若 Room 返回大量数据则可能导致帧丢失
updateUI(userList) // 仍然在主线程,但无长时间工作,ANR 风险低
}
}
关键点:Room 的 Flow 已经将数据库操作移至 IO 线程,但 collect 回调所在线程取决于上游的 flowOn 或调用协程的上下文。最佳实践是:对 DAO 返回的 Flow 应用 flowOn(Dispatchers.IO),确保数据库查询在 IO 线程执行,然后 collect 自动运行在调用协程的线程(通常为 Main)。
3.3 实现流程与代码演示
以下是一个完整的 Room DAO + Flow + 协程调度器配置示例:
// DAO 定义
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
// Repository 层
class UserRepository(private val userDao: UserDao) {
fun getUsers(): Flow<List<User>> = userDao.getAllUsers()
.flowOn(Dispatchers.IO) // 强制发射端在 IO 线程
}
// ViewModel 层(使用 Kotlin Flow 收集)
class UserViewModel(private val repository: UserRepository) : ViewModel() {
val users: StateFlow<List<User>> = repository.getUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
// 或者使用 collect 在主线程安全更新 UI
fun loadUsers(onSuccess: (List<User>) -> Unit) {
viewModelScope.launch {
repository.getUsers().collect { users ->
onSuccess(users) // 自动在主线程
}
}
}
}
// UI 层 Fragment/Activity
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.users.collect { users ->
adapter.submitList(users) // 安全在主线程更新 RecyclerView
}
}
}
}
常见错误:在 ViewModel 或 UI 层错误地使用
Dispatchers.Default 执行 Room 查询。虽然 Room 内部使用 IO 线程,但 flowOn(Dispatchers.Default) 会将发射任务移入 Default 线程池,与 IO 线程池争抢资源,且 Default 线程数量有限,可能降低数据库响应速度。Room 操作应始终绑定 Dispatchers.IO。
3.4 调试与验证
通过打印线程名即可确认调度器选择是否生效:
userDao.getAllUsers()
.flowOn(Dispatchers.IO)
.onStart { Log.d("Scheduler", "Emitting on ${Thread.currentThread().name}") }
.collect { value -> Log.d("Scheduler", "Collecting on ${Thread.currentThread().name}") }
// 输出类似:
// Emitting on DefaultDispatcher-worker-2 (IO thread)
// Collecting on main (Main thread)
若未使用 flowOn,则发射和收集都在主线程:
// 输出(未 flowOn):
// Emitting on main
// Collecting on main // Room 内部依旧在 IO 线程,但 Flow 发射时可能被阻塞
3.5 性能与稳定性优化
- 避免在主线程进行任何数据库操作:即使 Room 自动将查询放入 IO 线程,但
flowOn(Dispatchers.IO)应显式声明,确保兼容性和可读性。 - 使用
withContext(Dispatchers.IO)替代flowOn:对于一次性查询(非 Flow),使用suspend函数时推荐withContext(Dispatchers.IO)包裹。 - 合并
flowOn与buffer:当收集器较慢时,可在flowOn(Dispatchers.IO)后添加.buffer(Channel.CONFLATED)避免背压导致的内存膨胀。 - 线程数量监控:通过
Thread.activeCount()或 Android Profiler 观察 IO 线程池是否超出预期(默认上限 64 线程),必要时使用自定义线程池。
// 自定义 IO 线程池示例(仅在特殊场景需要)
val customIODispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
repository.getUsers()
.flowOn(customIODispatcher)
.collect { ... }
// 使用后务必调用 customIODispatcher.close() 释放资源
关键结论:在 Room + Flow 架构中,务必在 Repository 层显式添加
.flowOn(Dispatchers.IO),并在 ViewModel 或 UI 层使用 viewModelScope.launch 或 lifecycleScope.launch 触发收集。这样发射端运行在 IO 线程,收集端自动回到 Main 线程,既不会阻塞 UI,又能利用 IO 线程池的高并发能力。