文章目录
-
- 一、预备知识
-
- [1.1 同步和异步](#1.1 同步和异步)
-
- [1.1.1 同步](#1.1.1 同步)
- [1.1.2 异步](#1.1.2 异步)
- [1.2 异步编程](#1.2 异步编程)
-
- [1.2.1 异步编程的困境](#1.2.1 异步编程的困境)
- [1.2.2 解决方案](#1.2.2 解决方案)
- 二、协程
-
- [2.1 什么是协程](#2.1 什么是协程)
-
- [2.1.1 协程的定义](#2.1.1 协程的定义)
- [2.1.2 协程的特点](#2.1.2 协程的特点)
- [2.2 结构化并发](#2.2 结构化并发)
-
- [2.2.1 核心原则](#2.2.1 核心原则)
- [2.2.2 实现机制:Job 层次结构](#2.2.2 实现机制:Job 层次结构)
- [2.3 协程的基本概念](#2.3 协程的基本概念)
-
- [2.3.1 协程和挂起函数](#2.3.1 协程和挂起函数)
- [2.3.2 协程作用域 CoroutineScope](#2.3.2 协程作用域 CoroutineScope)
- [2.3.3 协程构建器 Coroutine Builder](#2.3.3 协程构建器 Coroutine Builder)
- [2.3.4 调度程序 Dispatcher](#2.3.4 调度程序 Dispatcher)
- [2.3.5 协程上下文 CoroutineContext](#2.3.5 协程上下文 CoroutineContext)
- 三、协程的高级功能
-
- [3.1 协程异常处理](#3.1 协程异常处理)
- [3.2 取消协程](#3.2 取消协程)
- 参考资料
一、预备知识
1.1 同步和异步
在计算机编程中,同步 和异步是描述操作执行方式的两种基本模式。
1.1.1 同步
同步操作:
- 阻塞式执行:调用后必须等待操作完成才能继续执行
- 直接返回结果:操作完成后立即返回结果
- 线程被占用:执行期间线程无法处理其他任务,资源利用效率较低。
- 代码顺序执行:逻辑上清晰直观
kotlin
// 同步操作示例
fun syncExample() {
println("开始读取文件...")
val content = readFileSync("data.txt") // 阻塞,直到文件读取完成
println("文件内容: $content") // 读取完成后才执行
println("继续执行其他操作")
}
1.1.2 异步
异步操作:
- 非阻塞式执行:调用后立即返回,不等待操作完成
- 回调通知结果:通过回调函数、Promise、事件等机制通知结果
- 线程可复用:执行期间线程可以处理其他任务
- 代码可能嵌套:多个异步操作可能导致回调嵌套
kotlin
// 异步操作示例
fun asyncExample() {
println("开始读取文件...")
readFileAsync("data.txt") { content -> // 立即返回,不阻塞
println("文件内容: $content") // 文件读取完成后回调执行
}
println("继续执行其他操作") // 立即执行,不等待文件读取
}
1.2 异步编程
1.2.1 异步编程的困境
在使用回调函数处理异步操作时,由于多个异步操作嵌套调用 导致代码形成深层嵌套结构,这种现象被称为"回调地狱"(Callback Hell)。这会导致如下问题:
- 代码可读性差:由于多层嵌套导致代码向右无限延伸以及业务逻辑被分散到多个回调函数中,代码难以阅读、调试和维护
- 错误处理分散:每个异步操作都需要单独的错误处理,但错误处理逻辑分散在各个层级,导致难以实现统一的错误恢复机制。
- 资源浪费:线程创建开销大
- 内存泄漏风险:需要手动管理生命周期。
- 取消困难:难以实现统一的取消逻辑
- 并发控制复杂:需要手动管理线程池,难以合理控制并发数量。
举个例子,用户获取访问令牌,然后获取用户资料并保存到本地,最后在界面上显示结果。这里面的每个操作都是耗时的I/O操作。由于Android 的主线程限制,必须使用回调在后台线程执行。回调地狱的案例如下:
这个流程一定要在后台执行(避免阻塞主线程)。至于在后台是用同步还是异步方式,都可以实现,但异步方式在资源利用上更高效,只是代码更复杂。
kotlin
// 传统回调(顺序执行)
fun loadUserDataSequential(userId: String) {
// 1. 获取用户信息
userApi.getUserInfo(userId, object : Callback<UserInfo> {
override fun onSuccess(userInfo: UserInfo) {
// 2. 获取用户订单
orderApi.getOrders(userInfo.id, object : Callback<List<Order>> {
override fun onSuccess(orders: List<Order>) {
// 3. 获取用户地址
addressApi.getAddresses(userInfo.id, object : Callback<List<Address>> {
override fun onSuccess(addresses: List<Address>) {
// 更新UI(3层嵌套)
updateUI(userInfo, orders, addresses)
}
override fun onError(e: Exception) {
showToast("获取地址失败")
}
})
}
override fun onError(e: Exception) {
showToast("获取订单失败")
}
})
}
override fun onError(e: Exception) {
showToast("获取用户信息失败")
}
})
}
1.2.2 解决方案
在 Android 开发中,常见的异步编程方案有:
- Thread + Handler:传统方式,子线程执行耗时任务,通过 Handler 在主线程更新 UI
- AsyncTask:Android 专用 API,已废弃,不推荐使用
- RxJava:第三方响应式编程库,链式调用
- LiveData + ViewModel + Repository:Jetpack 架构组件,结构化数据流与生命周期管理
- Kotlin 协程:官方推荐,以同步风格编写异步代码
目前,官方推荐的解决方案是 Kotlin 协程。
kotlin
// 协程版本:顺序风格(异步但顺序执行)
suspend fun loadUserDataSequential(userId: String) {
try {
val userInfo = userApi.getUserInfo(userId) // 第1步
val orders = orderApi.getOrders(userInfo.id) // 第2步
val addresses = addressApi.getAddresses(userInfo.id) // 第3步
updateUI(userInfo, orders, addresses) // 更新UI
} catch (e: Exception) {
showToast("加载失败: ${e.message}")
}
}
二、协程
2.1 什么是协程
2.1.1 协程的定义
协程是一种并发设计模式,用同步风格写异步代码。
协程可以与其他协程并发运行,并且可能并行运行。其底层仍运行在线程上,但通过挂起而非阻塞来高效利用线程 。

2.1.2 协程的特点
协程的主要特点:
- 轻量:协程是轻量级线程,可在一个线程上运行大量协程,切换开销极小。
- 挂起不阻塞:通过挂起(suspend)而非阻塞线程来等待,线程可复用处理其他任务。
- 可读性:用同步风格书写异步逻辑,执行顺序清晰,避免回调地狱。
- 结构化并发:协程与作用域绑定,生命周期可预测,取消可传递,避免任务泄漏。
- 生态集成 :Jetpack 库(Compose、ViewModel 等)原生支持协程,提供
viewModelScope、lifecycleScope等作用域。
2.2 结构化并发
结构化并发 (Structured Concurrency)是 Kotlin 协程用于管理和组织多个并发操作的核心设计原则。其核心思想是:为并发操作引入明确、可预测的结构,确保所有启动的协程在其所属作用域内完成,生命周期与父协程绑定,形成树状层次关系。
实现上,通过作用域(CoroutineScope) 和 Job 层次结构,将并发任务约束在特定生命周期内,避免任务被"遗忘"或"泄漏"。
2.2.1 核心原则
结构化并发基于以下四个基本原则,确保并发代码的可预测性:
- 启动:协程必须在限定了其生命周期的作用域(CoroutineScope)中启动。
- 完成:父协程(或作用域)必须等待其所有子协程完成后,自身才算完成。
- 取消 :取消操作向下传播。取消父协程会递归取消其所有子协程。但是,取消子协程却不会取消父协程。
- 失败 :失败操作向上传播。子协程的未捕获异常会取消父协程及其兄弟协程(默认 Job 行为)。
2.2.2 实现机制:Job 层次结构
结构化并发通过 Job 对象之间的父子关系实现。
(1) 什么是 Job
Job 是 Kotlin 协程中用于管理和控制协程生命周期的核心对象,可视为协程的"句柄"或引用。
-
生命周期管理 :Job 代表一个协程实例,可通过它启动、取消、等待完成,或查询状态(如是否运行、是否已完成)。
-
父子关系 :通过
launch或async启动协程时,返回的 Job 会自动成为当前作用域(父协程或 CoroutineScope)的子 Job。kotlinval job = launch { ... }
(2) 层次结构
父协程启动子协程时,子协程的 Job 自动成为父 Job 的子级,形成树状层次结构。
kotlin
val job = launch {
val childJob = launch { ... } // childJob 自动成为 job 的子级
}
每个 Job 都可再启动子 Job,层层嵌套。下图展示了典型的 Job 树状结构:

2.3 协程的基本概念
在 Android 中,典型流程是:后台线程执行耗时操作(如网络请求、数据库查询),然后在主线程更新 UI。示例:
kotlin
// ViewModel 中
fun loadUserProfile(userId: String) {
viewModelScope.launch {
_uiState.value = LoadingState
try {
val user = withContext(Dispatchers.IO) {
userApi.getUserById(userId)
}
_uiState.value = SuccessState(user)
} catch (e: Exception) {
_uiState.value = ErrorState(e.message)
}
}
}
下面介绍上述案例涉及的核心概念。
2.3.1 协程和挂起函数
- 协程 (Coroutines) :轻量级并发单元,用于编写并发代码。核心特性是挂起 (Suspend) 而不阻塞线程,一个线程可运行多个协程。
- 挂起函数 (Suspending Functions) :用
suspend关键字标记,只能在协程或其他挂起函数中调用。挂起函数需在协程构建器(如launch)内调用,而构建器又必须在CoroutineScope中启动。
2.3.2 协程作用域 CoroutineScope
CoroutineScope 是定义和管理协程生命周期边界的核心接口,是结构化并发的基础。
常见作用域:
GlobalScope:全局作用域,慎用(不绑定生命周期,易泄漏)viewModelScope:ViewModel 提供,随 ViewModel 销毁而取消lifecycleScope:LifecycleOwner 提供,随生命周期销毁而取消
2.3.3 协程构建器 Coroutine Builder
协程构建器 用于创建和启动协程,接受 suspend lambda(如 suspend () -> T 或 suspend CoroutineScope.() -> T)作为协程体。
| 构建器 | 核心功能 | 返回值 | 对调用者 | 典型场景 |
|---|---|---|---|---|
launch() |
启动并发任务,不关心返回值 | Job |
不阻塞,立即返回 | 后台操作(日志、事件)、结构化并发的子任务入口 |
async() |
启动并发计算,返回未来结果占位符 | Deferred<T> |
.await() 时挂起 |
并行执行多个 I/O 或计算,再组合结果 |
withContext() |
临时切换执行上下文(如线程),不启动新协程 | 代码块结果 | 挂起 | 主线程 → IO 线程请求 → 主线程更新 UI |
coroutineScope() |
创建结构化子作用域,组织并发子任务 | 代码块结果 | 挂起,等子协程完成 | 挂起函数内并行发起多个请求,全部完成后再返回 |
runBlocking() |
创建作用域并阻塞当前线程直到协程完成 | 代码块结果 | 阻塞线程 | main 或单元测试中桥接挂起函数。禁止在协程内使用 |
区分 :launch 和 async 是启动协程的构建器,必须在协程作用域 CoroutineScope 内调用;withContext 和 coroutineScope 是挂起函数,用于线程切换和任务组织,不启动顶层协程;runBlocking 仅用于测试或 main 入口。
2.3.4 调度程序 Dispatcher
调度程序决定协程在哪个线程或线程池上运行。Android 常用三种:
| 调度程序 | 用途 |
|---|---|
Dispatchers.Main |
主线程(UI 线程),用于界面操作和轻量任务 |
Dispatchers.IO |
磁盘/网络 I/O,共享线程池,适合文件、网络请求 |
Dispatchers.Default |
CPU 密集型计算(排序、图片处理),launch/async 未指定时的默认选择 |
2.3.5 协程上下文 CoroutineContext
CoroutineContext 是协程运行所需信息的集合,本质上是键值对 Map。核心元素:
Job:协程生命周期CoroutineDispatcher:调度到哪个线程CoroutineName:协程名称,便于调试CoroutineExceptionHandler:未捕获异常的处理
元素可通过 + 组合,如 Job() + Dispatchers.Main + CoroutineName("MyCoroutine")。子协程默认继承父协程的 CoroutineContext。
三、协程的高级功能
3.1 协程异常处理
异常处理遵循结构化并发:未捕获异常会向上传播。构建器分两类:
| 构建器 | 异常行为 | 处理方式 |
|---|---|---|
launch |
自动传播,立即失败 | 在协程体内部用 try-catch 捕获 |
async、produce |
暂不传播,等用户消费 | 在协程体内捕获,或在调用 .await() 处捕获 |
kotlin
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val job = GlobalScope.launch {
throw IndexOutOfBoundsException() // 未捕获则传播到默认异常处理器
}
job.join()
val deferred = GlobalScope.async {
throw ArithmeticException() // 不立即传播,等 await 时抛出
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught: $e")
}
}
在子协程内部捕获异常可实现细粒度控制,避免单任务失败影响整体。
3.2 取消协程
协程可被取消。取消具有隔离性:只作用于目标协程及其子协程,不影响同作用域内的兄弟协程,也不会反向取消父协程。
例如,假设用户在应用中选择了一项偏好设置,以指示自己不想在应用中再看到温度值了。他们只想知道天气预报信息(例如 Sunny),但不想知道确切温度。因此,要取消目前用于获取温度数据的协程。
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Weather forecast")
println(getWeatherReport())
println("Have a good day!")
}
suspend fun getWeatherReport() = coroutineScope {
val forecast = async { getForecast() }
val temperature = async { getTemperature() }
delay(200)
temperature.cancel() // 取消温度协程,forecast 不受影响
"${forecast.await()}" // 仅返回 "Sunny"
}
suspend fun getForecast(): String {
delay(1000)
return "Sunny"
}
suspend fun getTemperature(): String {
delay(1000)
return "30\u00b0C"
}
运行程序。现在,输出结果将如下所示。天气预报只包含天气预报信息 Sunny,但不包含温度,因为相应协程已取消。
Weather forecast
Sunny
Have a good day!