1. 协程基础:协程作用域、挂起函数、调度器、启动模式

1.1 原理:协程的轻量级异步抽象

Kotlin 协程是一种 轻量级并发框架,它通过挂起(suspend)而非阻塞线程来实现异步操作。与线程相比,协程可在单个线程内运行成千上万个并发任务,且切换开销极低。其核心思想是:将异步回调转换为顺序化的挂起调用,从而大幅提升代码可读性与维护性。

协程的四个基石概念——作用域(Scope)、挂起函数(Suspend Function)、调度器(Dispatcher)、启动模式(Launch Mode)——共同定义了协程的创建、执行、调度与生命周期管理方式。理解这四者的关系是掌握协程的第一步。

核心公式: 协程 = 作用域(生命周期) + 挂起函数(可暂停任务) + 调度器(线程池策略) + 启动模式(执行策略)
协程核心四要素关系图 协程核心四要素关系图 协程 Coroutine 作用域 Scope 管理生命周期 挂起函数 Suspend 可暂停的任务单元 调度器 Dispatcher 线程池分配策略 启动模式 LaunchMode 执行时机与策略 四个要素共同决定协程的行为:在哪里运行、何时运行、如何取消

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 实现流程:四步构建一个协程任务

  1. 选择作用域: 根据组件生命周期选定 viewModelScope / lifecycleScope / 自定义 CoroutineScope。
  2. 定义挂起函数: 将异步操作封装为 suspend fun,内部使用 withContext 切换调度器。
  3. 指定调度器: 在 launch 或 async 的参数中传入合适的 Dispatcher。
  4. 选择启动模式: 根据执行策略设置 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")
}