Kotlin 协程:别再写回调地狱了,用协程让异步代码像同步一样爽
协程是 Kotlin 异步编程的核心,也是现在 Android 开发必学的技能。这篇文章帮你从零搞懂协程的核心概念和实战用法。
1. 协程是什么
协程是一种「轻量级线程」,让异步代码看起来像同步代码。
对比传统异步方案:
// Java 回调地狱
api.getUser(object : Callback {
override fun onSuccess(user: User) {
api.getOrders(user.id, object : Callback {
override fun onSuccess(orders: List<Order>) {
api.getProducts(orders, object : Callback {
override fun onSuccess(products: List<Product>) {
// 终于拿到了
}
})
}
})
}
})
// Kotlin 协程:像写同步代码一样
suspend fun loadData() {
val user = api.getUser()
val orders = api.getOrders(user.id)
val products = api.getProducts(orders)
// 搞定!
}
2. 核心概念
挂起函数
suspend 修饰的函数可以「暂停」执行,不阻塞线程:
// 挂起函数
suspend fun fetchUser(): User {
delay(1000) // 模拟网络请求,暂停1秒
return User("张三", 25)
}
// 调用
suspend fun loadData() {
val user = fetchUser() // 这里会暂停,但不会卡住线程
println(user.name) // 1秒后继续执行
}
注意:suspend 函数只能在协程或其他挂起函数中调用。
CoroutineScope:协程作用域
协程必须在作用域中启动,作用域决定协程的生命周期:
// Android 中常用的作用域
viewModelScope.launch { } // ViewModel 销毁时取消
lifecycleScope.launch { } // Activity/Fragment 销毁时取消
3. 协程启动方式
launch:fire-and-forget
// 最常用:不阻塞,异步执行,不需要返回值
viewModelScope.launch {
val data = api.fetchData()
_uiState.value = data
}
返回值:Job,可以用来取消协程。
val job = viewModelScope.launch {
// 协程逻辑
}
// 取消协程
job.cancel()
job.join() // 等待取消完成
async:返回结果
// 需要返回值时用 async
viewModelScope.launch {
val user = async { api.fetchUser() }
val orders = async { api.fetchOrders() }
// await() 获取结果
println(user.await().name)
println(orders.await())
}
runBlocking:测试用
fun main() = runBlocking {
// 阻塞当前线程
val result = async { fetchData() }
println(result.await())
}
警告:不要在 Android 主线程使用 runBlocking,会导致 ANR。
4. 并行请求:async 的价值
串行 vs 并行
// ❌ 串行:总耗时 = 1000 + 800 + 600 = 2400ms
viewModelScope.launch {
val user = api.fetchUser() // 1000ms
val orders = api.fetchOrders() // 800ms
val banners = api.fetchBanners() // 600ms
}
// ✅ 并行:总耗时 ≈ 1000ms(最慢那个)
viewModelScope.launch {
val userDeferred = async { api.fetchUser() }
val ordersDeferred = async { api.fetchOrders() }
val bannersDeferred = async { api.fetchBanners() }
val user = userDeferred.await()
val orders = ordersDeferred.await()
val banners = bannersDeferred.await()
}
awaitAll:批量等待
viewModelScope.launch {
val (user, orders, banners) = awaitAll(
async { api.fetchUser() },
async { api.fetchOrders() },
async { api.fetchBanners() }
)
}
5. 结构化并发
协程的「结构化并发」特性保证:
- 子协程异常会导致父协程取消
- 父协程取消会自动取消所有子协程
- 页面销毁时自动清理所有协程
kotlin
viewModelScope.launch { // 父协程
val user = async { api.fetchUser() }
val orders = async { api.fetchOrders() }
// 如果 fetchOrders 抛异常,fetchUser 也会被取消
}
6. 异常处理
try-catch
viewModelScope.launch {
try {
val data = api.fetchData()
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
CoroutineExceptionHandler
kotlin
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Coroutine", "未捕获异常", exception)
}
viewModelScope.launch(handler) {
// 协程代码
}
7. Dispatchers:线程切换
kotlin
viewModelScope.launch {
// 默认在主线程
withContext(Dispatchers.IO) {
// 切换到 IO 线程执行耗时操作
val data = api.fetchData()
}
// 自动切回主线程,继续执行
_uiState.value = data
}
常用 Dispatchers:
表格
| Dispatcher | 用途 |
|---|---|
Main | 主线程,更新 UI |
IO | IO 操作,网络、数据库 |
Default | CPU 密集型计算 |
8. 实战模板
网络请求标准模板
class HomeViewModel : ViewModel() {
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val msg: String) : UiState()
}
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = withContext(Dispatchers.IO) {
api.fetchData()
}
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "加载失败")
}
}
}
}
并行请求模板
fun loadHomePage() {
viewModelScope.launch {
try {
val (user, orders, banners) = awaitAll(
async(Dispatchers.IO) { api.fetchUser() },
async(Dispatchers.IO) { api.fetchOrders() },
async(Dispatchers.IO) { api.fetchBanners() }
)
_uiState.value = UiState.Success(user, orders, banners)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "加载失败")
}
}
}
9. 常见问题
Q:launch 和 async 怎么选?
A:
- 不需要返回值 →
launch - 需要返回值,或要并行执行 →
async
Q:协程会内存泄漏吗?
A:使用 viewModelScope 或 lifecycleScope 不会,它们会在页面销毁时自动取消。不要用 GlobalScope。
Q:协程和 RxJava 怎么选?
A:新项目推荐协程,语法更简洁,生命周期管理更安全。老项目如果 RxJava 用得顺手,继续用也没问题。
总结
表格
| 概念 | 说明 |
|---|---|
suspend | 挂起函数,可在不阻塞线程的情况下暂停 |
launch | 启动协程,不返回结果 |
async | 启动协程,返回 Deferred,可获取结果 |
await() | 等待并获取 async 的结果 |
awaitAll() | 等待多个 async 完成 |
Dispatchers.IO | IO 线程,耗时操作 |
Dispatchers.Main | 主线程,UI 操作 |