纯干货,不废话,开整!
协程与线程之间对比
特性 | 协程 | 线程 |
---|---|---|
资源开销 | 轻量级(基于线程池复用) | 重量级(内核线程,MB 级内存) |
上下文切换 | 无系统调用,用户态完成 | 需要内核介入,开销大 |
并发模型 | 协作式(需手动挂起) | 抢占式(由系统调度) |
Android 主线程使用 | 通过 Dispatchers.Main 安全更新 UI | 直接操作需 runOnUiThread |
协程解决什么问题
相比于 Callback
、AsyncTask
这种历史出现过的异步任务处理方法,协程写法更加简洁。
- 使用同步写法,逻辑更加直观,避免主线程阻塞,防止 ANR。
- 解决回调地狱(Callback Hell)。
- 结构化并发(Structured Concurrency)自动绑定生命周期,防止内存泄漏。
- 简化并发任务协调,使用
async
启动任务,并在await()
中获取结果。 - 与
Jetpack
组件深度集成,有 Google 官方背书。
协程的优势是什么
- 代码简洁:用同步写法实现异步逻辑,告别回调嵌套。
- 生命周期安全:通过结构化并发自动管理资源,避免内存泄漏。
- 高效线程调度:智能切换线程(如 Dispatchers.Main 更新 UI)。
- 无缝整合生态:与 Retrofit、Room、LiveData 等 Jetpack 组件完美协作。
- 低开销:协程是用户态线程,切换成本远低于系统线程。
协程的底层实现原理
一句话概括,协程底层是基于挂起函数与状态机,结合线程调度和结构化并发机制实现的。
下面对这句话里面的核心概念进行解释和说明。
协程的核心:挂起函数与状态机
- 挂起函数(Suspend Functions):
- 通过 suspend 关键字标记的函数,可以在不阻塞当前线程的情况下暂停协程的执行。
- 挂起的本质是保存当前协程的上下文(如局部变量、执行位置),并让出线程资源,允许其他任务运行。
- 状态机(State Machine):
- Kotlin 编译器会将挂起函数转换为一个状态机。每个挂起函数被编译为一个类,内部通过 label 跟踪执行状态。
- 每次遇到挂起点(如
delay()
、withContext()
),状态机的label
会更新,并通过Continuation
接口保存和恢复上下文。
通过查看编译期生成的伪代码,可以观察到其内部是通过 label
标识状态的状态机实现。
kotlin
// 源码
suspend fun fetchData() {
val data = networkRequest() // 挂起点 1
process(data) // 挂起点 2
}
// 编译器生成的伪代码(状态机形式):
class FetchDataStateMachine : Continuation<Unit> {
var label = 0
var data: Data? = null
override fun resumeWith(result: Result<Unit>) {
when (label) {
0 -> {
label = 1
networkRequest(this) // 挂起,传递 Continuation
}
1 -> {
data = result.getOrThrow()
process(data)
label = 2
}
// ...
}
}
}
协程调度与线程切换
- 调度器(Dispatchers):
- 协程通过
Dispatchers
(如Main
、IO
、Default
)决定代码在哪个线程执行。- Dispatchers.Main:在 Android 主线程执行,底层通过
Handler.post()
或Looper
实现。 - Dispatchers.IO:使用线程池处理阻塞 I/O 操作(如网络请求、文件读写)。
- Dispatchers.Main:在 Android 主线程执行,底层通过
- 协程通过
- 线程切换:
- 当协程通过
withContext(Dispatchers.IO)
切换调度器时,底层会提交任务到目标线程池,挂起当前协程,并在目标线程恢复执行。
- 当协程通过
结构化并发与协程作用域
- 结构化并发(Structured Concurrency):
- 协程通过 CoroutineScope(如 viewModelScope、lifecycleScope)管理生命周期。
- 父协程取消时,所有子协程会自动取消,避免内存泄漏。
Job
与SupervisorJob
:Job
用于跟踪协程状态,SupervisorJob
允许子协程独立失败而不影响父协程(如处理独立 UI 组件)。
协程的底层实现库:kotlinx.coroutines
- 协程的启动:
- launch 或 async 启动协程时,会创建一个 Coroutine 对象,并将其调度到指定线程。
- 挂起与恢复:
- 协程挂起时,通过 Continuation.resumeWith() 恢复执行,切换回目标调度器。
协程异常传输机制
协程需要运行在 CoutineScope
上下文中,而 CoutineScope
是具备层级关系的,一个父 Scope 可以具有多个子 Scope。
普通Job------异常向上传播,取消父协程及兄弟协程
在默认场景下(使用普通Job),对协程异常的处理流程描述如下:
- 当子协程抛出未捕获的异常时,异常会向上传播到父协程。
- 父协程会因此被取消,并进一步取消其所有其他子协程。
- 最终,异常会传递到根协程,如果没有被处理(例如通过CoroutineExceptionHandler),可能导致程序崩溃。

图:普通Job异常传播流程
示例代码如下:
kotlin
runBlocking {
val parentJob = launch {
launch {
delay(100)
throw RuntimeException("子协程异常")
}
launch {
delay(200)
println("此协程可能不会执行") // 父协程被取消,此任务被中断
}
}
parentJob.join()
}
// 输出:父协程因异常终止,第二个子协程未执行
SupervisorJob------将异常隔离控制在子协程本身
Supervisor:导师,指导教师
在使用SupervisorJob或SupervisorScope时,则会截断异常透传过程,将异常隔离在子协程本身:
- 子协程的异常不会影响父协程或其他兄弟协程。
- 父协程不会被取消,其他子协程可以继续执行。
- 但抛出异常的子协程自身会被取消,其异常需要通过其他方式处理(例如在async中结合await捕获)。

图:SupervisorJob异常传递流程
在开发Android应用时,使用的viewModelScope本身就是基于SupervisorJob,已经具备异常隔离功能。
示例代码如下:
kotlin
runBlocking {
val supervisor = SupervisorJob()
launch(supervisor) {
launch {
delay(100)
throw RuntimeException("子协程异常")
}
launch {
delay(200)
println("此协程正常执行") // 不受异常影响
}
}
delay(300)
}
// 输出:第二个子协程正常执行
协程异常处理的最佳实践
协程的异常处理不同于线程,虽然知识点不复杂,但稍有不慎,就会造成异常逃逸、未按设想中的情况进行捕获的问题。
try-catch ------ 捕获同步和挂起函数中的异常
在协程内部直接使用try-catch块处理可能抛出异常的代码。
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
try {
delay(100)
throw RuntimeException("模拟异常")
} catch (e: Exception) {
println("捕获异常: ${e.message}")
}
}
delay(200) // 等待协程执行
}
// 输出:捕获异常: 模拟异常
CoroutineExceptionHandler ------ 处理未捕获异常
为协程作用域添加全局异常处理器,处理未被try-catch捕获的异常。根据协程异常的传播机制,该处理器也可自动捕获到子协程异常
kotlin
import kotlinx.coroutines.*
fun main() {
val handler = CoroutineExceptionHandler { _, e ->
println("全局捕获异常: ${e.message}")
}
val scope = CoroutineScope(Job() + handler)
scope.launch {
throw RuntimeException("未捕获的异常")
}
Thread.sleep(500) // 等待协程执行
}
// 输出:全局捕获异常: 未捕获的异常
SupervisorJob/supervisorScope ------ 隔离异常
子协程的异常不会影响父协程和其他子协程,这意味着子协程异常不会让父协程、兄弟协程取消,但未被捕获的异常仍然有可能导致应用崩溃。
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val supervisor = SupervisorJob()
val scope = CoroutineScope(coroutineContext + supervisor)
scope.launch {
delay(100)
throw RuntimeException("子协程1异常")
}
scope.launch {
delay(200)
println("子协程2正常执行")
}
delay(300)
}
// 输出:子协程2正常执行(但子协程1的异常未被处理,可能导致崩溃)
结合使用CoroutineExceptionHandler和SupervisorJob
在复杂场景中,全局处理器和异常隔离结合使用。
kotlin
import kotlinx.coroutines.*
fun main() {
val handler = CoroutineExceptionHandler { _, e ->
println("全局捕获异常: ${e.message}")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("子协程1异常")
}
scope.launch {
delay(200)
println("子协程2正常执行")
}
Thread.sleep(500)
}
// 输出:
// 全局捕获异常: 子协程1异常
// 子协程2正常执行
处理async协程中的异常------在await()时进行捕获
使用async启动协程时,异常会在调用await()时抛出,需在此处捕获。
kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(100)
throw RuntimeException("async任务异常")
}
try {
deferred.await()
} catch (e: Exception) {
println("捕获async异常: ${e.message}")
}
}
// 输出:捕获async异常: async任务异常
协程异常处理小结
- 作用域选择:
- 使用SupervisorJob或supervisorScope隔离子协程异常。
- viewModelScope和lifecycleScope已内置SupervisorJob。
- 异常捕获方式:
- 同步代码:直接使用try-catch。
- 全局异常:通过CoroutineExceptionHandler。
- async任务:在await()时捕获异常。
- Android注意事项:
- 避免在子协程中未处理异常导致应用崩溃。
- 结合LiveData或StateFlow传递错误状态到UI层。