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 主线程,用于更新 UIDispatchers.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)
- 定义挂起函数
suspend fun fetchUserData(userId: String): User {
delay(1000) // 模拟网络请求
return User(id = userId, name = "Test User")
}
- 组合挂起函数
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()) // 并行调用
}
取消协程
- 取消协程
val job = lifecycleScope.launch {
while (isActive) { // 检查协程是否活跃
delay(500)
// 执行重复任务
}
}
// 取消协程
job.cancel()
// 取消并等待完成
job.cancelAndJoin()
- 超时处理
lifecycleScope.launch {
try {
withTimeout(5000) {
// 如果5秒内未完成则抛出TimeoutCancellationException
fetchData()
}
} catch (e: TimeoutCancellationException) {
// 处理超时
}
}
// withTimeoutOrNull超时返回null,不抛异常
val result = withTimeoutOrNull(5000) { fetchData() }
异常处理
- try/catch 捕获异常
lifecycleScope.launch {
try {
riskyOperation()
} catch (e: Exception) {
// 处理异常
}
}
- CoroutineExceptionHandler
val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("捕获到异常: ${e.message}")
}
val scope = CoroutineScope(Dispatchers.Main + exceptionHandler)
scope.launch {
throw Exception("测试异常")
}
- 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
切换调度器)时:- 当前协程从原线程暂停,状态保存到续体中。
- 调度器将续体提交到新的目标线程(如
Dispatchers.Main
)。 - 新线程执行续体,恢复协程的执行。
2. 常见调度器的实现
Dispatchers.Main
:Android 中绑定主线程(通过Looper.getMainLooper()
),使用Handler
切换到主线程执行。Dispatchers.IO
:基于线程池(可缓存的线程池),专门处理 IO 操作(网络、数据库等)。Dispatchers.Default
:基于固定大小的线程池(核心数为 CPU 核心数),处理 CPU 密集型任务。
四、结构化并发(Structured Concurrency)
协程通过作用域(CoroutineScope) 实现结构化并发,核心是:
- 父协程会等待所有子协程完成后再结束。
- 父协程取消时,所有子协程会被自动取消。
- 异常会从子协程向上传播到父协程。
这一机制通过 Job
实现:每个协程都关联一个 Job
,父 Job 会跟踪所有子 Job,形成一个树形结构。当父 Job 取消时,会递归取消所有子 Job。
五、总结
Kotlin 协程的核心原理可概括为:
- 编译期转换:
suspend
函数被转换为带有续体参数的状态机,实现状态保存与恢复。 - 非阻塞挂起:通过续体回调实现挂起 / 恢复,避免线程阻塞。
- 调度器:负责协程在不同线程间的切换,实现高效的异步执行。
- 结构化并发:通过作用域和 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)
}
四、并发控制与优化
- 指定调度器
并发任务通常需要在后台线程执行,可给 async
指定调度器:
async(Dispatchers.IO) { fetchData() } // 在IO线程池执行
- 取消并发任务
若某个任务失败,可取消其他未完成的任务:
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()
}
}
- 限制并发数量
对于大量任务(如批量上传),可通过 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。
五、核心总结
- 内层与外层的关系:外层协程默认等待所有内层子协程完成(结构化并发)。
- 顺序执行条件:
- 内层协程通过 launch { … }.join() 启动(先启动再等待)。
- 直接调用挂起函数(内部协程会被 coroutineScope 等待)。
- 并发执行条件:
- 内层协程通过 async { … } 启动(不立即 await,或最后 awaitAll)。
- 内层协程通过 launch { … } 启动但不立即 join(最后统一 join)。
- 多层嵌套:每层的执行方式独立决定,内层的并发 / 顺序不影响外层,反之亦然。
通过灵活组合这些方式,可以实现复杂的异步流程控制,既保证代码清晰,又能最大化利用资源。