1. 什么是协程
协程(Coroutine)是轻量级的线程,支持挂起和恢复,从而避免阻塞线程。
2. 协程的优势
协程通过结构化并发和简洁的语法,显著提升了异步编程的效率与代码质量。
2.1 资源占用低(一个线程可运行多个协程)
传统多线程模型中,每个线程需要独立的系统资源(如内存栈),而协程共享线程资源。
- 高效线程利用:通过调度器(如 Dispatchers.IO),一个线程池可同时处理数千个协程任务(如并发网络请求或文件读写)。
- 减少上下文切换:协程挂起时不会阻塞线程,线程可立即执行其他协程任务,减少线程切换的性能损耗。
2.2 代码可读性强(顺序编写异步逻辑)
协程通过同步代码风格实现异步逻辑,彻底消除“回调地狱”。
- 同步化表达:使用挂起函数(如 withContext、await())可将异步操作写成顺序执行的代码。
- 结构化并发:通过 CoroutineScope 管理协程生命周期,自动取消子协程,避免内存泄漏。
3. 协程的核心组件
协程通过一组核心组件实现结构化并发和高效的任务管理。
3.1 CoroutineScope(作用域)
- 管理协程的生命周期,确保协程在特定范围内启动和取消。
- 通过结构化并发避免资源泄漏。
3.1.1 常见作用域:
- lifecycleScope : 与 Lifecycle(如 Activity/Fragment)绑定,界面销毁时自动取消所有子协程。
- viewModelScope : 与 ViewModel 绑定,ViewModel 销毁时自动取消协程,适合处理业务逻辑。
3.2 CoroutineContext(上下文)
定义协程的上下文信息,如线程调度器、协程名称、异常处理器等。
- Job : 控制协程的生命周期(启动、取消、监控状态)
- Dispatcher : 指定协程运行的线程
- CoroutineName : 为协程命名,便于调试
3.3 Dispatcher (调度器)
指定协程运行的线程
- Dispatchers.Main : 主线程,用于更新 UI 或执行轻量级操作。
注意:在非 Android 环境(如单元测试)中可能不存在。 - Dispatchers.IO : 适用于 IO 密集型任务(如网络请求、数据库读写、文件操作)。
底层机制:共享线程池,默认最小 64 线程。 - Dispatchers.Default : 适用于 CPU 密集型任务(如排序、计算、图像处理)。
底层机制:线程数与 CPU 核心数相同。
3.4 Job (作业)
3.4.1 Job (作业)
表示一个协程任务,不返回结果,通过 launch 创建。
val job = launch { /* ... */ }
job.start() // 启动(默认自动启动)
job.cancel() // 取消
job.join() // 挂起当前协程,等待此 Job 完成
3.4.2 Deferred (异步结果)
Job 的子类,表示一个会返回结果的异步任务,通过 async 创建。
val deferred = async { fetchData() }
val data = deferred.await() // 挂起协程直到结果就绪
4. 协程构建器
协程构建器是创建和启动协程的入口点,不同构建器适用于不同场景。
4.1 launch:启动一个不返回结果的协程
启动一个不返回结果的协程,适用于“触发后无需等待结果”的任务(如日志上报、缓存清理)。
特性
- 返回 Job 对象,用于控制协程生命周期(取消、监控状态)。
- 默认继承父协程的上下文(如作用域、调度器)。
// 在 ViewModel 中启动一个后台任务
fun startBackgroundTask() {
viewModelScope.launch(Dispatchers.IO) {
cleanCache() // 在 IO 线程执行清理操作
log("Cache cleaned") // 完成后记录日志
}
// 无需等待结果,直接执行后续代码
}
4.2 async:并发执行并获取结果
启动一个返回结果的协程,适用于需要并行执行多个任务并汇总结果的场景。
特性:
- 返回 Deferred 对象,通过 await() 挂起并获取结果。
- 可通过 async 启动多个协程后统一等待结果,提升执行效率。
示例:并行请求多个接口并合并数据
viewModelScope.launch {
// 同时发起两个网络请求
val userDeferred = async(Dispatchers.IO) { fetchUser() }
val postsDeferred = async(Dispatchers.IO) { fetchPosts() }
// 等待两个请求完成(总耗时取决于最慢的任务)
val user = userDeferred.await()
val posts = postsDeferred.await()
// 合并结果并更新 UI
showUserProfile(user, posts)
}
4.3 runBlocking:在阻塞代码中启动协程
阻塞当前线程,直到其内部的协程执行完毕。
主要用于测试,或在非协程环境中临时调用挂起函数。
示例:在单元测试中测试协程逻辑
@Test
fun testFetchData() = runBlocking {
// 阻塞当前线程,等待协程完成
val data = fetchData() // 直接调用挂起函数
assertEquals(expectedData, data)
}
应避免在主线程使用 runBlocking,因为会阻塞主线程 !
5. 挂起函数
挂起函数(Suspending Function)是协程的核心特性之一,允许协程在非阻塞的前提下暂停和恢复执行。挂起函数只能在协程或其他挂起函数中调用,适用于需要等待异步操作完成的场景。
5.1 delay():协程的“非阻塞休眠”
delay() 会暂停协程的执行指定时间(单位:毫秒),期间不会阻塞线程,线程可执行其他任务。
5.1.1 与 Thread.sleep() 的区别:
delay() | Thread.sleep() |
---|---|
挂起协程,释放线程资源 | 阻塞线程,线程无法执行其他任务 |
只能在协程或挂起函数中调用 | 可在任何线程中调用 |
viewModelScope.launch {
repeat(10) {
delay(1000) // 每隔 1 秒执行一次,不阻塞主线程
updateCounter(it)
}
}
5.2 withContext():灵活的线程切换
在指定协程上下文(如 Dispatcher)中执行代码块,完成后自动恢复原上下文。替代传统回调或 Handler,简化线程切换逻辑。
suspend fun loadData() {
// 在 IO 线程执行网络请求
val data = withContext(Dispatchers.IO) {
api.fetchData()
}
// 自动切回调用方的上下文(如 Main 线程)
updateUI(data)
}
5.2.1 与 async 的区别
- withContext:直接返回结果,适用于单次切换线程的串行任务。
- async:返回 Deferred,适用于并行任务。
避免嵌套多层 withContext,可用 async 替代以提升并发效率。
5.3 await():安全获取异步结果
挂起协程,等待 Deferred 任务完成并返回结果。若 Deferred 任务出现异常,await() 会抛出该异常。
5.3.1 示例:并行任务与结果合并
viewModelScope.launch {
val task1 = async(Dispatchers.IO) { fetchDataA() }
val task2 = async(Dispatchers.IO) { fetchDataB() }
// 同时等待两个任务完成
val combinedData = combineData(task1.await(), task2.await())
}
5.3.2 await()怎么处理异常
使用 try-catch 捕获 await() 的异常:
val deferred = async { /* 可能抛出异常的代码 */ }
try {
val result = deferred.await()
} catch (e: Exception) {
handleError(e)
}
若需取消任务,调用 deferred.cancel()
5.3.3 协程是否存活
通过 coroutineContext.isActive
检查协程是否存活,可以及时终止无效操作
suspend fun heavyCalculation() {
withContext(Dispatchers.Default) {
for (i in 0..100000) {
if (!isActive) return@withContext // 检查协程是否被取消
// 执行计算
}
}
}
6. 协程的异常处理机制
6.1 异常传播机制
默认规则:
- 子协程异常会向上传播:当子协程抛出未捕获的异常时,父协程会立即取消,进而取消所有其他子协程。
- 兄弟协程受影响:若子协程 A 抛出异常,其兄弟协程 B 也会被取消,即使 B 仍在执行中。
示例:未捕获异常导致父协程取消
viewModelScope.launch {
// 子协程 1
launch {
delay(100)
throw IOException("网络请求失败") // 未捕获异常
}
// 子协程 2(会被父协程取消)
launch {
repeat(10) {
delay(200)
log("子任务执行中") // 仅执行 1 次后父协程取消
}
}
}
6.2 捕获异常的方式
6.2.1 方式 1:try-catch 块
在协程内部直接捕获异常,适用于同步代码逻辑。
viewModelScope.launch {
try {
fetchData() // 可能抛出异常的挂起函数
} catch (e: IOException) {
showError("网络异常: ${e.message}")
}
}
6.2.2 方式 2:CoroutineExceptionHandler
全局异常处理器,用于捕获未通过 try-catch 处理的异常。
定义异常处理器
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
log("未捕获异常: ${throwable.message}")
showErrorToast() // 例如弹出 Toast
}
附加到协程上下文
viewModelScope.launch(exceptionHandler) {
launch { throw IOException() } // 异常会被 exceptionHandler 捕获
}
仅在根协程(直接通过 launch 或 async 创建的顶层协程)中生效。
6.3 隔离异常:SupervisorJob
阻止子协程的异常传播到父协程,避免“一颗老鼠屎坏了一锅粥”。常用于独立任务场景(如同时发起多个不相关的网络请求)。
- 子协程的失败不会影响其他子协程。
- 父协程仍会等待所有子协程完成(除非显式取消)。
6.3.1 通过 SupervisorJob() 创建作用域
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch { throw Exception() } // 不影响其他子协程
scope.launch { delay(1000) } // 正常执行
6.3.2 在现有作用域中使用 supervisorScope
viewModelScope.launch {
supervisorScope {
launch { throw IOException() } // 仅自身失败
launch { delay(1000) } // 继续执行
}
}
6.4 自定义异常处理策略
可以根据业务需求设计容错逻辑,例如:
- 重试机制:在捕获异常后自动重试任务。
- 回退操作:失败时返回默认值或缓存数据。
6.4.1 示例:网络请求重试
suspend fun fetchDataWithRetry(retries: Int = 3): Data {
repeat(retries) { attempt ->
try {
return api.fetchData()
} catch (e: IOException) {
if (attempt == retries - 1) throw e // 最后一次重试仍失败则抛出异常
delay(1000 * (attempt + 1)) // 延迟后重试(指数退避)
}
}
throw IllegalStateException("Unreachable")
}
6.5 协程异常的最佳实践
6.5.1 明确异常边界
- 在协程根节点或关键入口处统一处理异常(如使用 CoroutineExceptionHandler)。
- 避免在底层函数中静默吞没异常(如 catch 后不处理)。
6.5.2 区分取消与异常
- 使用 isActive 检查协程状态,及时终止无效任务。
- 通过 ensureActive() 快速失败
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
6.5.3 谨慎使用 SupervisorJob
仅当子协程完全独立时使用,避免隐藏潜在问题。