第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 调度器切换流程 UI 层 (Fragment/Activity) collect / stateIn Dispatchers.Main ViewModel 调用 repository viewModelScope (Main) Repository flowOn(Dispatchers.IO) 发射切换至 IO 线程 Room DAO SQLite 查询 内部已使用 IO 线程 SQLite 数据库文件 磁盘 IO 操作 使用 Dispatchers.IO 线程池 collect 自动回到调用协程的调度器(Main) ✅ 调度器切换使数据库操作始终在 IO 线程,UI 更新始终在主线程,避免 ANR
关键结论:在 Room + Flow 架构中,务必在 Repository 层显式添加 .flowOn(Dispatchers.IO),并在 ViewModel 或 UI 层使用 viewModelScope.launch 或 lifecycleScope.launch 触发收集。这样发射端运行在 IO 线程,收集端自动回到 Main 线程,既不会阻塞 UI,又能利用 IO 线程池的高并发能力。