Kotlin 协程:别再写回调地狱了,用协程让异步代码像同步一样爽

7 阅读3分钟

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. 结构化并发

协程的「结构化并发」特性保证:

  1. 子协程异常会导致父协程取消
  2. 父协程取消会自动取消所有子协程
  3. 页面销毁时自动清理所有协程

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
IOIO 操作,网络、数据库
DefaultCPU 密集型计算

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:使用 viewModelScopelifecycleScope 不会,它们会在页面销毁时自动取消。不要用 GlobalScope

Q:协程和 RxJava 怎么选?

A:新项目推荐协程,语法更简洁,生命周期管理更安全。老项目如果 RxJava 用得顺手,继续用也没问题。

总结

表格

概念说明
suspend挂起函数,可在不阻塞线程的情况下暂停
launch启动协程,不返回结果
async启动协程,返回 Deferred,可获取结果
await()等待并获取 async 的结果
awaitAll()等待多个 async 完成
Dispatchers.IOIO 线程,耗时操作
Dispatchers.Main主线程,UI 操作