1. 协程基础:协程作用域、挂起函数、调度器、启动模式
1.1 原理:协程的轻量级异步抽象
Kotlin 协程是一种 轻量级并发框架,它通过挂起(suspend)而非阻塞线程来实现异步操作。与线程相比,协程可在单个线程内运行成千上万个并发任务,且切换开销极低。其核心思想是:将异步回调转换为顺序化的挂起调用,从而大幅提升代码可读性与维护性。
协程的四个基石概念——作用域(Scope)、挂起函数(Suspend Function)、调度器(Dispatcher)、启动模式(Launch Mode)——共同定义了协程的创建、执行、调度与生命周期管理方式。理解这四者的关系是掌握协程的第一步。
核心公式: 协程 = 作用域(生命周期) + 挂起函数(可暂停任务) + 调度器(线程池策略) + 启动模式(执行策略)
1.2 协程作用域:生命周期与结构化并发
CoroutineScope 是协程的 生命周期容器。每个协程都必须在一个作用域内启动,作用域负责跟踪所有子协程,并在自身结束时自动取消它们。这种设计称为 结构化并发,它防止了协程泄漏。
| 作用域类型 | 默认调度器 | 典型用途 |
|---|---|---|
| GlobalScope | Dispatchers.Default | 全局单例,生命周期与应用相同,不推荐在Android中使用 |
| ViewModelScope | Dispatchers.Main | 绑定 ViewModel 生命周期,ViewModel 销毁时自动取消 |
| LifecycleScope | Dispatchers.Main | 绑定 Activity/Fragment 生命周期,界面销毁时自动取消 |
| 自定义 CoroutineScope | 由开发者指定 | 管理特定业务组件的协程生命周期 |
⚠️ 重要: 在 Android 中禁止使用
GlobalScope 启动协程。它不与任何组件生命周期绑定,容易导致协程泄漏、内存占用过高甚至应用崩溃。始终使用 viewModelScope 或 lifecycleScope。
1.2.1 自定义作用域的创建方式
// 方式一:通过 CoroutineScope() 工厂函数
val myScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// 方式二:通过 MainScope() 便捷函数(内部使用 SupervisorJob + Main)
val uiScope = MainScope()
// 方式三:在 ViewModel 中使用 viewModelScope(需要引入 lifecycle-viewmodel-ktx)
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
// 协程体
}
}
}
1.3 挂起函数:可暂停的异步单元
suspend 函数 是协程中最基本的执行单元。它可以在执行过程中 挂起(暂停) 而不阻塞当前线程,并在合适的时机 恢复 继续执行。挂起函数只能在协程内部或其他挂起函数中调用。
// 定义一个挂起函数:模拟网络请求
suspend fun fetchUserInfo(userId: String): User {
// withContext 切换到 IO 线程执行网络请求
return withContext(Dispatchers.IO) {
delay(1000) // 模拟网络延迟(挂起,不阻塞线程)
User(userId, "Alice")
}
}
// 在协程中调用挂起函数
viewModelScope.launch {
val user = fetchUserInfo("001") // 挂起等待结果
// 协程恢复,继续执行
updateUI(user)
}
💡 调试技巧: 使用
kotlinx.coroutines.debug 依赖,可以在日志中看到协程的名称和挂起点,便于追踪异步执行流程。
1.4 调度器:决定协程在哪个线程池运行
CoroutineDispatcher 负责将协程分发到合适的线程上执行。Kotlin 提供了四种内置调度器,覆盖绝大多数场景。
| 调度器 | 线程池 | 适用场景 |
|---|---|---|
| Dispatchers.Main | 主线程(UI 线程) | 界面更新、LiveData 观察、UI 交互 |
| Dispatchers.IO | 弹性线程池(默认 64 个线程) | 网络请求、文件读写、数据库操作 |
| Dispatchers.Default | CPU 密集型线程池(线程数 = CPU 核心数) | 大数据计算、图片压缩、JSON 解析 |
| Dispatchers.Unconfined | 不固定线程 | 仅用于单元测试或极短操作,不推荐生产使用 |
// 调度器切换示例:IO 读取 → Default 解析 → Main 更新
viewModelScope.launch(Dispatchers.IO) {
val rawData = file.readText() // IO 线程
val result = withContext(Dispatchers.Default) {
parseLargeJson(rawData) // CPU 密集型计算
}
withContext(Dispatchers.Main) {
updateUI(result) // 主线程更新 UI
}
}
1.5 启动模式:控制协程的执行时机
CoroutineStart 枚举定义了协程的启动策略。在使用 launch 或 async 时,可以通过 start 参数指定启动模式。
| 启动模式 | 行为描述 | 典型使用 |
|---|---|---|
| CoroutineStart.DEFAULT | 立即创建并调度执行(可被取消) | 大多数常规场景 |
| CoroutineStart.LAZY | 惰性启动,直到调用 start() 或 await() 才执行 |
按需加载、条件触发任务 |
| CoroutineStart.ATOMIC | 立即执行,且第一个挂起点之前不可取消 | 需要原子性执行初始操作的场景 |
| CoroutineStart.UNDISPATCHED | 在当前线程立即执行到第一个挂起点,之后才重新调度 | 减少不必要的线程切换开销 |
// LAZY 启动模式示例:按需执行
val lazyJob = viewModelScope.launch(start = CoroutineStart.LAZY) {
fetchUserInfo("002")
}
// 用户点击按钮时才真正执行
button.setOnClickListener {
lazyJob.start()
}
// ATOMIC 启动模式示例:初始操作不可取消
viewModelScope.launch(start = CoroutineStart.ATOMIC) {
// 这段代码在第一个挂起点之前不会被取消
markTaskStarted()
delay(100) // 第一个挂起点,之后可取消
processTask()
}
1.6 实现流程:四步构建一个协程任务
- 选择作用域: 根据组件生命周期选定
viewModelScope/lifecycleScope/ 自定义CoroutineScope。 - 定义挂起函数: 将异步操作封装为
suspend fun,内部使用withContext切换调度器。 - 指定调度器: 在
launch或async的参数中传入合适的Dispatcher。 - 选择启动模式: 根据执行策略设置
CoroutineStart(默认为DEFAULT)。
// 完整四步实现示例
class UserViewModel : ViewModel() {
// 步骤1:使用 ViewModelScope(自动绑定生命周期)
fun loadUserProfile(userId: String) {
viewModelScope.launch(
// 步骤3:指定调度器为 IO
context = Dispatchers.IO,
// 步骤4:启动模式为 DEFAULT(默认)
start = CoroutineStart.DEFAULT
) {
// 步骤2:调用挂起函数
val user = fetchUserFromApi(userId)
// 切换回主线程更新 UI
withContext(Dispatchers.Main) {
_userLiveData.value = user
}
}
}
private suspend fun fetchUserFromApi(userId: String): User {
// 模拟网络请求
delay(800)
return User(userId, "Alice", "alice@example.com")
}
}
1.7 调试方法:识别协程的执行轨迹
在 Android Studio 中调试协程,推荐以下三种手段:
- 启用协程调试标记: 在
AndroidManifest.xml的<application>中添加android:debuggable="true",并在代码中调用CoroutineScope.coroutineContext[CoroutineName]命名协程。 - 使用
CoroutineName上下文: 为协程命名,便于在日志和堆栈中识别。 - 日志输出当前线程与协程信息:
// 为协程命名以便调试
viewModelScope.launch(Dispatchers.IO + CoroutineName("FetchProfile")) {
Log.d("CoroutineDemo", "当前线程: ${Thread.currentThread().name}")
// 输出示例: 当前线程: DefaultDispatcher-worker-1
val user = fetchUserFromApi("001")
withContext(Dispatchers.Main) {
_userLiveData.value = user
}
}
1.8 常见错误与避坑指南
| 错误场景 | 原因 | 正确做法 |
|---|---|---|
| 在非协程中调用挂起函数 | 挂起函数只能在协程或另一个挂起函数中调用 | 使用 launch 或 async 包裹,或使用 runBlocking(仅测试用) |
使用 GlobalScope 导致协程泄漏 |
作用域不绑定生命周期,无法自动取消 | 改用 viewModelScope / lifecycleScope |
在 Dispatchers.Main 执行耗时操作 |
阻塞主线程导致 ANR | 使用 withContext(Dispatchers.IO) 切换 |
| 忘记处理协程内的异常 | 未捕获异常会导致应用崩溃 | 使用 try-catch 或 CoroutineExceptionHandler |
在 LAZY 模式下忘记调用 start() |
协程永远不会执行 | 确保调用 job.start() 或 deferred.await() |
1.9 性能与稳定性优化建议
- 合理选择调度器: IO 密集型任务使用
Dispatchers.IO,CPU 密集型使用Dispatchers.Default,避免资源争抢。 - 控制协程并发数: 使用
Semaphore或limitedParallelism限制同时运行的协程数量,防止 IO 线程池过载。 - 优先使用
SupervisorJob: 在自定义作用域中使用SupervisorJob,使子协程的失败不影响兄弟协程。 - 及时取消不再需要的协程: 利用作用域的自动取消机制,避免协程在组件销毁后继续运行。
// 使用 SupervisorJob 隔离子协程异常
val safeScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
safeScope.launch {
// 子协程1 异常不会影响子协程2
throw RuntimeException("Oops")
}
safeScope.launch {
// 这个协程会正常执行
delay(100)
println("Still running")
}