协程基础
本教程共 100 篇 · 第 18 篇 · 更新于 2026-07-28 · 约 9 分钟阅读
18. 协程基础
本节目标:理解协程是什么、为什么 Android 用它处理异步、学会用 launch/async/withContext 写异步代码,知道 Dispatchers 怎么选。
为什么需要协程
Android 主线程(UI 线程)一旦被阻塞超过 5 秒,系统就弹「应用无响应」(ANR)。所以耗时操作(网络、数据库、文件 IO)必须放后台线程。
传统方案各有痛点:
- Thread:重量级,创建销毁开销大,不好取消。
- AsyncTask:已废弃(API 30 起),代码难读,容易泄漏。
- Handler / Thread + Runnable:回调嵌套,地狱。
- RxJava:强大但学习曲线陡,链式调用复杂。
Kotlin 协程用「挂起」代替「阻塞」,让你写异步代码像写同步代码一样顺。轻量(一个线程能跑十万个协程)、内置取消、结构化并发,是 Android 异步的现代标准。
协程是什么
协程(Coroutine)是一种并发设计模式,不是线程。可以理解为「轻量级的线程」:
- 多个协程跑在一个线程上,通过挂起(suspend)让出执行权,不阻塞线程。
- 挂起时协程的状态被保存,恢复后接着跑。
- 一个线程上能同时挂十万个协程,开销远小于十万个线程。
生活化比喻:协程像在厨房同时做几道菜。炖汤要等 30 分钟,你不会傻站着(阻塞),而是切菜、炒肉(挂起后干别的),汤好了回来继续。线程是厨房,协程是菜谱流程。
添加依赖
build.gradle.kts(Module :app)里加:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
// 版本号用 Version Catalog 管理更好
}
Android 项目通常还要协程和 lifecycle、ViewModel 集成:
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
suspend 函数
suspend 关键字标记可挂起的函数,只能在协程里或其他 suspend 函数里调用:
suspend fun fetchUser(userId: String): User {
delay(1000) // 模拟网络延迟,不阻塞线程
return User(userId, "张三")
}
delay()是 suspend 版的Thread.sleep(),挂起当前协程但不阻塞线程。- suspend 函数能像普通函数一样顺序调用,没有回调嵌套。
suspend fun loadAndShow() {
val user = fetchUser("u1") // 看起来同步,实际挂起等结果
println(user.name)
val posts = fetchPosts(user) // 接着干
println(posts)
}
Note
- suspend 函数不指定在哪个线程跑,由调用它的协程上下文决定。这是协程灵活的地方,也是初学者要理解的点。
协程构建器:launch 与 async
要在普通代码里启动协程,用协程构建器。
launch
启动一个不返回结果的协程(类似「发射后不管」):
import kotlinx.coroutines.*
fun main() = runBlocking { // runBlocking 创建协程作用域,测试用
launch {
delay(1000)
println("World!")
}
println("Hello,")
}
// 输出:
// Hello,
// World!
launch 返回一个 Job,可以 cancel() 或 join()。
async
启动一个返回结果的协程,用 await() 拿结果:
suspend fun loadData(): Pair<User, List<Post>> = coroutineScope {
val userDeferred = async { fetchUser("u1") }
val postsDeferred = async { fetchPosts() }
// 两个请求并行,等两个都完成
Pair(userDeferred.await(), postsDeferred.await())
}
async 启动后立刻开始执行,await() 时如果没完成就挂起等。两个 async 配合能并行执行多个任务。
Tip
- 需要结果用
async,不需要结果(如打日志、发埋点)用launch。要并行多个任务用多个async+awaitAll。
Dispatchers:协程跑在哪个线程
Dispatchers 决定协程在哪个线程或线程池执行。常用的:
| Dispatcher | 用途 |
|---|---|
Dispatchers.Main | 主线程(UI),Android 上更新 UI 必须在这 |
Dispatchers.IO | IO 线程池(网络、文件、数据库),专为阻塞 IO 设计 |
Dispatchers.Default | CPU 密集任务(排序、解析、计算),线程数等于 CPU 核数 |
Dispatchers.Unconfined | 不指定,跟调用方同线程(少用) |
指定方式:
launch(Dispatchers.IO) {
val data = fetchData() // 在 IO 线程
withContext(Dispatchers.Main) {
textView.text = data // 切回主线程更新 UI
}
}
withContext:切换线程
withContext 切换协程到指定 Dispatcher,执行完块后切回原 Dispatcher。是切线程的标准方式:
suspend fun fetchAndDisplay() {
val data = withContext(Dispatchers.IO) {
// 这块在 IO 线程
api.getData()
}
// 这里回到原 Dispatcher(如果原是 Main,就在主线程)
showData(data)
}
Note
- 别用
Thread.sleep()或阻塞 API 在Dispatchers.Main里。需要阻塞就withContext(Dispatchers.IO)切过去。
协程作用域(CoroutineScope)
协程跑在作用域里,作用域管理其内所有协程的生命周期。作用域取消时,里面所有协程都取消。这叫结构化并发。
常用作用域
// 1. runBlocking:阻塞当前线程直到协程完成,主要用于测试
runBlocking {
launch { /* ... */ }
}
// 2. coroutineScope:挂起当前协程直到子协程完成,suspend 函数里用
suspend fun loadAll() = coroutineScope {
launch { /* ... */ }
async { /* ... */ }
}
// 3. GlobalScope:全局作用域,生命周期跟应用一样长
// 慎用!容易泄漏,官方不推荐
GlobalScope.launch { /* ... */ }
Android 提供的作用域
AndroidX lifecycle 库给 ViewModel、Activity、Fragment 提供了现成作用域:
// ViewModel 里
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch { // ViewModel 销毁时自动取消
val data = repo.fetch()
_state.value = data
}
}
}
// Activity / Fragment 里
lifecycleScope.launch { // 生命周期结束自动取消
val data = repo.fetch()
// 更新 UI
}
Warning
- 不要用
GlobalScope.launch,它没有自动取消机制,Activity 销毁了协程还在跑,会泄漏甚至崩溃。用viewModelScope或lifecycleScope。
取消协程
协程支持协作式取消。Job.cancel() 后,协程在下个挂起点退出:
val job = launch {
repeat(1000) { i ->
println("处理 $i")
delay(100) // 挂起点,这里检查取消
}
}
delay(500)
job.cancel() // 取消协程
job.join() // 等待完全退出
cancelAndJoin() 是两者合一。
协作式取消的注意点
CPU 密集循环里没有挂起点,cancel 不会立即生效,要主动检查:
val job = launch(Dispatchers.Default) {
var i = 0
while (i < Int.MAX_VALUE && isActive) { // isActive 检查取消
i++
}
}
ensureActive() 或 yield() 也能插入取消检查点。
异常处理
协程里抛异常会传播到父协程。async 的异常在 await() 时才抛出。
try {
val result = async { throw RuntimeException("网络错误") }.await()
} catch (e: Exception) {
// 处理异常
}
顶层协程建议用 CoroutineExceptionHandler:
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Coroutine", "崩溃了", exception)
}
viewModelScope.launch(handler) {
// 异常会被 handler 捕获
}
Tip
- Android 里推荐用
try-catch包住可能失败的协程代码,比依赖全局 handler 更可控。SupervisorJob用于「子协程失败不影响其他子协程」的场景,第 74 章进阶讲。
Android 实战示例
class UserViewModel(
private val repo: UserRepository
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_state.value = UiState.Loading
try {
// 并行加载用户和文章
val userDeferred = async { repo.fetchUser(userId) }
val postsDeferred = async { repo.fetchPosts(userId) }
val user = userDeferred.await()
val posts = postsDeferred.await()
_state.value = UiState.Success(UserData(user, posts))
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "加载失败")
}
}
}
}
// Repository 里
class UserRepository(private val api: ApiService) {
suspend fun fetchUser(id: String): User {
return withContext(Dispatchers.IO) {
api.getUser(id) // Retrofit 协程适配,本身就是 suspend
}
}
}
小结
协程是轻量并发方案,用挂起代替阻塞。suspend 函数只能在协程里调。launch 启动无返回值协程,async 启动有返回值协程(await 取结果)。Dispatchers.Main 更新 UI,Dispatchers.IO 做网络/文件,Dispatchers.Default 做 CPU 计算。withContext 切线程。Android 用 viewModelScope 和 lifecycleScope,结构化并发自动取消。第 2 部分结束,下一部分学应用基础与工程结构。