Android Kotlin 协程

Kotlin 协程是一种轻量级的线程管理方案,特别常用于处理异步操作(如网络请求、数据库操作等),相比传统的回调或 AsyncTask 更简洁易读。

基本使用

添加依赖

build.gradle 中添加协程依赖:

dependencies{implementation'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'}

启动方式

launch – 启动不返回结果的协程

val scope = CoroutineScope(Dispatchers.Main)
val job = scope.launch {
    // 执行任务
    delay(1000)
    println("任务完成")
}

async/await – 启动返回结果的协程

lifecycleScope.launch {
    // 并行执行两个任务
    val deferred1 = async(Dispatchers.IO) { fetchData1() }
    val deferred2 = async(Dispatchers.IO) { fetchData2() }
    
    // 等待结果并处理
    val result1 = deferred1.await()
    val result2 = deferred2.await()
    handleResults(result1, result2)
}

runBlocking – 阻塞当前线程直到协程完成

// 通常用于测试或main函数
fun main() = runBlocking {
    launch {
        delay(1000)
        println("协程完成")
    }
    println("主线程等待")
}

协程作用域(CoroutineScope)

预定义作用域

  • GlobalScope:全局作用域,生命周期与应用一致,不推荐直接使用
  • lifecycleScope:与 Activity/Fragment 生命周期绑定(需导入 lifecycle-viewmodel-ktx)
  • viewModelScope:与 ViewModel 生命周期绑定(需导入 lifecycle-viewmodel-ktx)
/ Activity/Fragment中使用,与生命周期绑定
lifecycleScope.launch { ... }

// ViewModel中使用,与ViewModel生命周期绑定
viewModelScope.launch { ... }

// 全局作用域(谨慎使用)
GlobalScope.launch { ... }

自定义作用域

// 自定义作用域,可控制生命周期
val customScope = CoroutineScope(Dispatchers.Main + Job())

// 在适当的时候取消
customScope.cancel()

调度器(Dispatchers)

  • Dispatchers.Main:Android 主线程,用于更新 UI
  • Dispatchers.IO:用于 IO 操作(网络、数据库、文件读写)
  • Dispatchers.Default:用于 CPU 密集型操作
  • Dispatchers.Unconfined:不指定线程,由当前线程执行
launch(Dispatchers.Main) {
    // 主线程:更新UI
}

launch(Dispatchers.IO) {
    // IO线程:网络请求、数据库操作
}

launch(Dispatchers.Default) {
    // 默认线程池:CPU密集型任务
}

launch(Dispatchers.Unconfined) {
    // 不限制线程:从当前线程开始执行
}

// 切换调度器
launch(Dispatchers.IO) {
    val data = fetchData()
    withContext(Dispatchers.Main) {
        updateUI(data) // 切换到主线程
    }
}

挂起函数(suspend)

  1. 定义挂起函数
suspend fun fetchUserData(userId: String): User {
    delay(1000) // 模拟网络请求
    return User(id = userId, name = "Test User")
}
  1. 组合挂起函数
suspend fun getFullUserInfo(userId: String): UserInfo {
    val user = fetchUserData(userId) // 串行调用
    val posts = fetchUserPosts(userId)
    return UserInfo(user, posts)
}

suspend fun getFullUserInfoParallel(userId: String): UserInfo = coroutineScope {
    val userDeferred = async { fetchUserData(userId) }
    val postsDeferred = async { fetchUserPosts(userId) }
    UserInfo(userDeferred.await(), postsDeferred.await()) // 并行调用
}

取消协程

  1. 取消协程
val job = lifecycleScope.launch {
    while (isActive) { // 检查协程是否活跃
        delay(500)
        // 执行重复任务
    }
}

// 取消协程
job.cancel()
// 取消并等待完成
job.cancelAndJoin()
  1. 超时处理
lifecycleScope.launch {
    try {
        withTimeout(5000) {
            // 如果5秒内未完成则抛出TimeoutCancellationException
            fetchData()
        }
    } catch (e: TimeoutCancellationException) {
        // 处理超时
    }
}

// withTimeoutOrNull超时返回null,不抛异常
val result = withTimeoutOrNull(5000) { fetchData() }

异常处理

  1. try/catch 捕获异常
lifecycleScope.launch {
    try {
        riskyOperation()
    } catch (e: Exception) {
        // 处理异常
    }
}
  1. CoroutineExceptionHandler
val exceptionHandler = CoroutineExceptionHandler { _, e ->
    println("捕获到异常: ${e.message}")
}

val scope = CoroutineScope(Dispatchers.Main + exceptionHandler)
scope.launch {
    throw Exception("测试异常")
}
  1. SupervisorJob 隔离异常
// 子协程异常不会影响其他子协程和父协程
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

scope.launch {
    throw Exception("这个异常不会取消其他协程")
}

scope.launch {
    // 这个协程会正常执行
}

协程上下文组合

// 组合调度器和异常处理器
val context = Dispatchers.IO + exceptionHandler + Job()
val scope = CoroutineScope(context)

// 继承父上下文并修改部分参数
launch(Dispatchers.IO) {
    // 继承父作用域的上下文,使用IO调度器
    withContext(Dispatchers.Main) {
        // 临时切换到Main调度器
    }
}

通道(Channel)- 协程间通信

lifecycleScope.launch {
    val channel = Channel<Int>()
    
    // 发送方
    launch {
        for (i in 1..5) {
            channel.send(i)
            delay(500)
        }
        channel.close() // 关闭通道
    }
    
    // 接收方
    launch {
        for (value in channel) {
            println("收到: $value")
        }
    }
}

流(Flow)- 响应式数据流

// 定义Flow
fun fetchNews(): Flow<News> = flow {
    while (true) {
        val news = api.getLatestNews()
        emit(news) // 发送数据
        delay(60000) // 每分钟刷新一次
    }
}.flowOn(Dispatchers.IO) // 指定执行线程

// 收集Flow
lifecycleScope.launch {
    fetchNews()
        .filter { it.isImportant } // 过滤
        .map { it.toUiModel() } // 转换
        .collect { news ->
            updateNewsUI(news) // 收集数据更新UI
        }
}

select 表达式 – 同时等待多个挂起函数

suspend fun selectFirstResponse() {
    val result = select<String> {
        async { fetchFromServerA() }.onAwait { it }
        async { fetchFromServerB() }.onAwait { it }
    }
    println("第一个响应: $result")
}

Mutex – 协程同步

val mutex = Mutex()
var count = 0

suspend fun increment() {
    mutex.withLock {
        // 临界区,确保并发安全
        count++
    }
}

原理

Kotlin 协程的原理可以从编译期转换、状态机实现、调度机制等多个层面来理解,核心是通过非阻塞的挂起 / 恢复机制实现高效的异步编程。

一、协程的本质

协程不是线程,也不是操作系统级别的资源,而是运行在线程上的一段可暂停、可恢复的代码片段。其核心能力是:

  • 挂起(Suspend):在不阻塞当前线程的情况下暂停执行,并保存当前执行状态(局部变量、调用栈等)。
  • 恢复(Resume):在挂起操作完成后(如网络请求返回),从暂停点继续执行。

相比线程,协程的创建和切换成本极低(几乎是内存级操作),因此可以创建成千上万个协程而不影响性能。

二、挂起函数(suspend)的原理

suspend 是 Kotlin 协程的关键字,标记的函数可以在协程中被暂停。但其本质并非 “暂停”,而是编译器生成的状态保存与恢复逻辑

1. 挂起函数的编译期转换

当编译器遇到 suspend 函数时,会进行特殊处理:

  • 为函数添加一个隐藏参数 Continuation<T>(续体),用于接收挂起后的结果并恢复执行。
  • 将函数体转换为状态机,每个挂起点对应一个状态。

例如,一个简单的挂起函数:

suspend fun loadData(): String {
    val config = fetchConfig()  // 挂起点1
    val result = fetchData(config)  // 挂起点2
    return result
}

编译器会将其转换为类似以下的结构(伪代码):

// 续体接口:保存状态并恢复执行
interface Continuation<T> {
    val context: CoroutineContext  // 协程上下文
    fun resumeWith(result: Result<T>)  // 恢复执行的回调
}

// 编译后的 loadData 函数
fun loadData(continuation: Continuation<String>): Any {
    // 状态机实现(保存局部变量和当前执行状态)
    val stateMachine = continuation as? LoadDataStateMachine ?: LoadDataStateMachine(continuation)
    
    return when (stateMachine.state) {
        0 -> {
            // 第一次执行:调用 fetchConfig 并挂起
            stateMachine.state = 1  // 更新状态
            fetchConfig(stateMachine)  // 传入状态机作为续体
        }
        1 -> {
            // 从 fetchConfig 恢复:获取结果并执行下一步
            stateMachine.config = stateMachine.result as Config
            stateMachine.state = 2  // 更新状态
            fetchData(stateMachine.config, stateMachine)  // 传入状态机作为续体
        }
        2 -> {
            // 从 fetchData 恢复:返回最终结果
            stateMachine.result as String
        }
        else -> error("Invalid state")
    }
}

// 状态机类:保存执行状态和局部变量
class LoadDataStateMachine(
    private val continuation: Continuation<String>
) : Continuation<Any> {
    var state = 0
    var config: Config? = null
    var result: Any? = null  // 保存挂起操作的结果
    
    override fun resumeWith(result: Result<Any>) {
        this.result = result.getOrThrow()
        // 调用 loadData 继续执行(此时状态已更新)
        loadData(this).let {
            if (it !is CoroutineSingletons.COROUTINE_SUSPENDED) {
                continuation.resumeWith(Result.success(it as String))
            }
        }
    }
}

2. 挂起的标志:COROUTINE_SUSPENDED

当挂起函数需要暂停时,会返回一个特殊值 COROUTINE_SUSPENDED,告诉调用者 “当前协程已挂起,后续会通过续体恢复”。

如果函数能立即返回结果(无需挂起),则直接返回结果,不触发状态机切换。

三、协程的调度机制

协程的执行依赖调度器(Dispatcher),其作用是决定协程在哪个线程(或线程池)上执行。

1. 调度器的工作流程

  • 协程启动时,调度器将其任务提交到目标线程(如 Dispatchers.IO 对应的线程池)。
  • 当执行到挂起函数(如 withContext 切换调度器)时:
    1. 当前协程从原线程暂停,状态保存到续体中。
    2. 调度器将续体提交到新的目标线程(如 Dispatchers.Main)。
    3. 新线程执行续体,恢复协程的执行。

2. 常见调度器的实现

  • Dispatchers.Main:Android 中绑定主线程(通过 Looper.getMainLooper()),使用 Handler 切换到主线程执行。
  • Dispatchers.IO:基于线程池(可缓存的线程池),专门处理 IO 操作(网络、数据库等)。
  • Dispatchers.Default:基于固定大小的线程池(核心数为 CPU 核心数),处理 CPU 密集型任务。

四、结构化并发(Structured Concurrency)

协程通过作用域(CoroutineScope) 实现结构化并发,核心是:

  • 父协程会等待所有子协程完成后再结束。
  • 父协程取消时,所有子协程会被自动取消。
  • 异常会从子协程向上传播到父协程。

这一机制通过 Job 实现:每个协程都关联一个 Job,父 Job 会跟踪所有子 Job,形成一个树形结构。当父 Job 取消时,会递归取消所有子 Job。

五、总结

Kotlin 协程的核心原理可概括为:

  1. 编译期转换suspend 函数被转换为带有续体参数的状态机,实现状态保存与恢复。
  2. 非阻塞挂起:通过续体回调实现挂起 / 恢复,避免线程阻塞。
  3. 调度器:负责协程在不同线程间的切换,实现高效的异步执行。
  4. 结构化并发:通过作用域和 Job 管理协程生命周期,确保资源安全。

这种设计让协程既保持了同步代码的可读性,又具备了异步操作的高效性,成为 Android 异步编程的首选方案。

单层执行顺序

在 Kotlin 协程中,并发和顺序执行是通过不同的协程启动和组合方式实现的。这两种执行方式各有适用场景,下面详细介绍其实现方法和原理:

一、顺序执行(串行执行)

顺序执行指多个任务按先后 顺序依次执行,前一个任务完成后才开始下一个任务。这是协程中最自然的执行方式,直接通过挂起函数的调用顺序实现。

实现方式

直接在同一个协程中按顺序调用挂起函数:

lifecycleScope.launch {
    // 任务1:获取用户信息(耗时操作)
    val user = fetchUser()  // 挂起,等待完成后再执行下一步
    println("获取用户信息完成: ${user.name}")
    
    // 任务2:根据用户ID获取订单(依赖任务1的结果)
    val orders = fetchOrders(user.id)  // 继续挂起
    println("获取订单完成: ${orders.size}个")
    
    // 任务3:更新UI(依赖前两个任务的结果)
    updateUI(user, orders)
}

// 挂起函数定义(模拟耗时操作)
suspend fun fetchUser(): User {
    delay(1000)  // 模拟网络请求
    return User(id = "1", name = "Alice")
}

suspend fun fetchOrders(userId: String): List<Order> {
    delay(800)  // 模拟网络请求
    return listOf(Order(id = "101"), Order(id = "102"))
}

特点

  • 任务按代码顺序执行,总耗时 = 各任务耗时之和(上例约 1800ms)。
  • 后一个任务可以直接使用前一个任务的结果(天然支持依赖关系)。
  • 所有任务运行在协程启动时指定的调度器线程(除非中途用 withContext 切换)。

二、并发执行(并行执行)

并发执行指多个任务同时启动,各自独立运行,总耗时接近耗时最长的那个任务。协程中通过 async 配合 await 实现。

实现方式

使用 async 启动多个子协程,再通过 await 获取结果:

lifecycleScope.launch {
    // 并发启动两个任务(立即执行,不等待)
    val userDeferred = async { fetchUser() }  // 返回 Deferred<User>
    val goodsDeferred = async { fetchRecommendGoods() }  // 返回 Deferred<List<Goods>>
    
    // 等待所有任务完成并获取结果(总耗时约 1000ms,取最长任务耗时)
    val user = userDeferred.await()
    val goods = goodsDeferred.await()
    
    // 处理结果
    updateHomeUI(user, goods)
}

// 挂起函数定义
suspend fun fetchUser(): User {
    delay(1000)  // 任务1耗时1000ms
    return User(id = "1", name = "Alice")
}

suspend fun fetchRecommendGoods(): List<Goods> {
    delay(800)  // 任务2耗时800ms
    return listOf(Goods(id = "g1"), Goods(id = "g2"))
}

特点

  • 任务同时启动,总耗时 ≈ 耗时最长的任务(上例约 1000ms)。
  • 需通过 Deferred.await() 显式等待结果(async 返回 Deferred 对象,代表未来的结果)。
  • 适合无依赖关系的任务(如首页同时加载用户信息和推荐商品)。

三、混合执行(部分并发 + 部分顺序)

实际开发中常需要结合两种方式:先并行执行独立任务,再顺序处理依赖任务的结果。

lifecycleScope.launch {
    // 阶段1:并发执行两个独立任务
    val userDeferred = async { fetchUser() }
    val configDeferred = async { fetchConfig() }
    
    // 等待阶段1结果(并行)
    val user = userDeferred.await()
    val config = configDeferred.await()
    
    // 阶段2:顺序执行依赖阶段1结果的任务
    val orders = fetchOrders(user.id)  // 依赖user
    val filteredOrders = filterOrders(orders, config.filterRule)  // 依赖orders和config
    
    updateUI(filteredOrders)
}

四、并发控制与优化

  1. 指定调度器

并发任务通常需要在后台线程执行,可给 async 指定调度器:

async(Dispatchers.IO) { fetchData() }  // 在IO线程池执行
  1. 取消并发任务

若某个任务失败,可取消其他未完成的任务:

val scope = CoroutineScope(Dispatchers.Main)
val job = scope.launch {
    val deferred1 = async { task1() }
    val deferred2 = async { task2() }
    
    try {
        deferred1.await()
        deferred2.await()
    } catch (e: Exception) {
        // 取消所有子任务
        job.cancel()
    }
}
  1. 限制并发数量

对于大量任务(如批量上传),可通过 Semaphore 限制并发数:

val semaphore = Semaphore(3)  // 最多同时执行3个任务
val tasks = List(10) { index ->
    async {
        semaphore.withPermit {  // 获取许可后执行,满了则等待
            uploadFile(index)
        }
    }
}
tasks.awaitAll()  // 等待所有完成

五、核心原理

  • 顺序执行:利用挂起函数的特性,前一个 suspend 函数未完成时,后一个不会执行(状态机在挂起点暂停,恢复后才进入下一状态)。
  • 并发执行async 会立即启动新的子协程,与父协程或其他子协程并行运行(本质是多个状态机在不同线程或同一线程的不同时间片交替执行)。
  • await() 的作用:阻塞当前协程(非阻塞线程),等待 Deferred 对应的子协程完成,再获取结果并继续执行。

通过灵活组合顺序和并发执行方式,可高效处理各种异步场景,既保证代码可读性,又能最大化利用系统资源。

嵌套执行顺序

协程嵌套时的顺序和并发执行逻辑,取决于嵌套内部协程的启动方式(launch/async)以及作用域的关系。核心原则是:内层协程的执行方式由其启动方式决定,而外层协程会根据作用域规则等待内层协程完成。

一、嵌套协程的基本规则

父协程与子协程的关系

当在一个协程(父)中启动另一个协程(子)时,子协程会成为父协程的子任务,父协程会默认等待所有子协程完成后再结束(结构化并发特性)。

顺序 vs 并发的关键

若内层协程通过直接调用挂起函数或 launch 后立即 join 启动,则为顺序执行。

若内层协程通过 async 启动(不立即 await),则为并发执行。

二、嵌套中的顺序执行

内层协程按顺序执行,即外层协程会等待当前内层协程完成后,再执行下一个内层协程。

场景 1:直接调用挂起函数(隐式嵌套)

挂起函数内部可能包含协程逻辑,但对外层协程而言,会等待其执行完成才继续。

lifecycleScope.launch {
    // 外层协程
    println("外层开始")
    
    // 调用挂起函数(内部可能有嵌套协程)
    taskA()  // 等待taskA完全完成(包括其内部协程)
    taskB()  // 只有taskA完成后才执行
    
    println("外层结束")
}

// 挂起函数(内部包含协程)
suspend fun taskA() = coroutineScope {
    println("taskA开始")
    launch { 
        delay(1000)  // 子协程1
        println("taskA的子协程1完成") 
    }
    launch { 
        delay(500)   // 子协程2
        println("taskA的子协程2完成") 
    }
    // coroutineScope会等待所有内部子协程完成才返回
    println("taskA结束")
}

suspend fun taskB() {
    println("taskB开始")
    delay(800)
    println("taskB结束")
}

执行顺序:

外层开始
taskA开始
taskA的子协程2完成(500ms后)
taskA的子协程1完成(1000ms后)
taskA结束
taskB开始
taskB结束(1800ms后)
外层结束

特点:

  • taskA 内部的两个子协程并发执行(总耗时 1000ms)。
  • 外层协程会等待 taskA 完全完成(包括其内部所有子协程)后,才执行 taskB,因此整体是顺序执行。

场景 2:显式 launch + join

通过 launch 启动子协程后,用 join 等待其完成,再执行下一个子协程。

lifecycleScope.launch {
    println("外层开始")
    
    // 子协程1:启动后立即等待完成
    val job1 = launch { 
        delay(1000)
        println("子协程1完成") 
    }
    job1.join()  // 等待子协程1完成
    
    // 子协程2:仅在子协程1完成后启动
    val job2 = launch { 
        delay(500)
        println("子协程2完成") 
    }
    job2.join()
    
    println("外层结束")
}

执行顺序:

外层开始
子协程1完成(1000ms后)
子协程2完成(1500ms后)
外层结束

特点:

  • 子协程 1 和子协程 2顺序执行(总耗时 1500ms)。
  • join() 会阻塞当前协程(非阻塞线程),直到子协程完成。

三、嵌套中的并发执行

内层协程同时启动,外层协程等待所有内层协程完成,但内层协程之间互不等待。

场景 1:async 启动子协程(推荐)

用 async 启动多个子协程,外层通过 awaitAll() 等待所有结果,子协程之间并发执行。

lifecycleScope.launch {
    println("外层开始")
    
    // 并发启动两个子协程
    val deferred1 = async { 
        delay(1000)
        println("子协程1完成") 
        "结果1" 
    }
    val deferred2 = async { 
        delay(500)
        println("子协程2完成") 
        "结果2" 
    }
    
    // 等待所有子协程完成并获取结果
    val results = awaitAll(deferred1, deferred2)
    println("所有子协程完成,结果:$results")
    
    println("外层结束")
}

执行顺序:

外层开始
子协程2完成(500ms后)
子协程1完成(1000ms后)
所有子协程完成,结果:[结果1, 结果2]
外层结束

特点:

  • 子协程 1 和子协程 2并发执行(总耗时 1000ms)。
  • awaitAll() 会等待所有 Deferred 完成,适合需要收集所有结果的场景。

场景 2:launch 启动但不立即 join

用 launch 启动子协程后不立即 join,而是在最后统一等待,子协程之间并发执行。

lifecycleScope.launch {
    println("外层开始")
    
    // 启动两个子协程(立即执行,不等待)
    val job1 = launch { 
        delay(1000)
        println("子协程1完成") 
    }
    val job2 = launch { 
        delay(500)
        println("子协程2完成") 
    }
    
    // 等待所有子协程完成
    job1.join()
    job2.join()  // 此时job2可能已完成,直接通过
    
    println("外层结束")
}

执行顺序:与 async 示例一致,总耗时 1000ms。

特点:适合不需要返回结果的并发任务。

四、多层嵌套的复杂场景

当协程嵌套超过两层时,执行逻辑仍遵循上述规则,即每层的并发 / 顺序由该层的启动方式决定

lifecycleScope.launch {
    println("外层开始")
    
    // 第一层:并发启动两个子协程
    val job1 = launch {
        println("中层1开始")
        // 第二层:顺序执行两个任务
        delay(800)  // 任务1
        println("中层1的任务1完成")
        delay(400)  // 任务2(依赖任务1)
        println("中层1完成")
    }
    
    val job2 = launch {
        println("中层2开始")
        // 第二层:并发执行两个任务
        async { 
            delay(600) 
            println("中层2的任务1完成") 
        }
        async { 
            delay(900) 
            println("中层2的任务2完成") 
        }.await()  // 等待任务2(最长耗时任务)
        println("中层2完成")
    }
    
    // 等待所有中层协程完成
    job1.join()
    job2.join()
    
    println("外层结束")
}

执行顺序:

外层开始
中层1开始
中层2开始
中层2的任务1完成(600ms后)
中层1的任务1完成(800ms后)
中层2的任务2完成(900ms后)
中层2完成
中层1的任务2完成(1200ms后)
中层1完成
外层结束

逻辑解析:

  • 外层协程:并发启动 中层1 和 中层2(总耗时由较慢的 中层1 决定,1200ms)。
  • 中层 1:内部两个任务顺序执行(800ms + 400ms = 1200ms)。
  • 中层 2:内部两个任务并发执行(最长 900ms),因此中层 2 总耗时 900ms。

五、核心总结

  1. 内层与外层的关系:外层协程默认等待所有内层子协程完成(结构化并发)。
  2. 顺序执行条件
  • 内层协程通过 launch { … }.join() 启动(先启动再等待)。
  • 直接调用挂起函数(内部协程会被 coroutineScope 等待)。
  1. 并发执行条件
  • 内层协程通过 async { … } 启动(不立即 await,或最后 awaitAll)。
  • 内层协程通过 launch { … } 启动但不立即 join(最后统一 join)。
  1. 多层嵌套:每层的执行方式独立决定,内层的并发 / 顺序不影响外层,反之亦然。

通过灵活组合这些方式,可以实现复杂的异步流程控制,既保证代码清晰,又能最大化利用资源。