返回博客

Kotlin Coroutines 最佳实践

协程(Coroutines)是 Kotlin 最强大的特性之一,它让异步编程变得像同步代码一样简洁。但在实际使用中,很多开发者容易踩坑:内存泄漏、异常丢失、线程阻塞等问题频发。

本文基于实际项目经验,总结 Kotlin 协程的核心概念、最佳实践和常见陷阱。

核心优势:代码简洁、性能优秀、异常安全、易于测试

为什么需要协程

传统异步编程的痛点:

协程通过挂起函数结构化并发解决了这些问题。

核心概念

1. 挂起函数(suspend function)

// 普通函数
fun fetchData(): String {
    // 阻塞线程
    return api.getData()
}

// 挂起函数
suspend fun fetchData(): String {
    // 挂起而不阻塞线程
    return withContext(Dispatchers.IO) {
        api.getData()
    }
}
关键区别:挂起函数可以在协程中暂停执行而不阻塞线程,普通函数会阻塞调用线程。

2. CoroutineScope

协程作用域,用于管理协程的生命周期:

作用域使用场景生命周期
viewModelScopeAndroid ViewModelViewModel 销毁时自动取消
lifecycleScopeActivity/Fragment生命周期结束时取消
CoroutineScope()自定义作用域需要手动取消

3. Dispatcher

// Main:主线程,用于 UI 操作
withContext(Dispatchers.Main) {
    textView.text = result
}

// IO:磁盘/网络 IO
withContext(Dispatchers.IO) {
    val data = database.query()
}

// Default:CPU 密集型计算
withContext(Dispatchers.Default) {
    val result = heavyComputation()
}

最佳实践

1. 优先使用挂起函数

// ❌ 不推荐:返回 Deferred
fun loadData(): Deferred = async { repo.getData() }

// ✅ 推荐:直接返回挂起函数
suspend fun loadData(): Data = repo.getData()
原则:挂起函数是协程的"一等公民",Deferred 只用于需要组合多个异步操作的场景。

2. 结构化并发

// ❌ 不推荐:手动管理 Job
val job = Job()
val scope = CoroutineScope(job + Dispatchers.IO)
scope.launch { ... }
// 容易忘记取消

// ✅ 推荐:使用结构化并发
viewModelScope.launch {
    // 自动取消,无需手动管理
    val data = repo.getData()
}

3. 异常处理

// ✅ 推荐:在作用域边界处理异常
viewModelScope.launch {
    try {
        val data = repo.getData()
        _uiState.value = UiState.Success(data)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e.message)
    }
}

// ✅ 使用 supervisorScope 隔离异常
supervisorScope {
    launch {
        // 这个协程失败不会影响其他协程
        api.getUser()
    }
    launch {
        // 这个协程仍然可以正常执行
        api.getPosts()
    }
}

4. 并发操作

// ✅ 并行执行多个独立操作
val (user, posts) = coroutineScope {
    awaitAll(
        async { api.getUser() },
        async { api.getPosts() }
    )
}

// ✅ 使用 zip 组合两个结果
val result = coroutineScope {
    async { api.getUser() }
        .zip(async { api.getPosts() }) { user, posts ->
            UserWithPosts(user, posts)
        }
}

5. 超时和重试

// ✅ 超时处理
withTimeout(5000) {
    api.slowOperation()
}

// ✅ 超时返回 null
val result = withTimeoutOrNull(5000) {
    api.slowOperation()
}

// ✅ 重试机制
suspend fun  retry(
    times: Int,
    initialDelay: Long = 100,
    maxDelay: Long = 1000,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: Exception) {
            delay(currentDelay)
            currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
        }
    }
    return block() // 最后一次尝试
}

常见陷阱

陷阱 1:在挂起函数中阻塞线程
Thread.sleep() 会阻塞线程,应该使用 delay()
陷阱 2:忘记处理异常
launch 中的异常会被吞掉,需要用 try-catchCoroutineExceptionHandler
陷阱 3:滥用 GlobalScope
GlobalScope 没有生命周期管理,容易导致内存泄漏,应该使用 viewModelScope 或自定义作用域
陷阱 4:在主线程执行 IO 操作
即使使用协程,也要用 withContext(Dispatchers.IO) 切换线程

实战案例

Android ViewModel 中的协程

class UserViewModel(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow>(UiState.Loading)
    val uiState: StateFlow> = _uiState.asStateFlow()

    init {
        loadUser()
    }

    private fun loadUser() {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            
            try {
                // 并行加载用户数据和头像
                val (user, avatar) = coroutineScope {
                    awaitAll(
                        async { userRepository.getUser() },
                        async { userRepository.getAvatar() }
                    )
                }
                
                _uiState.value = UiState.Success(user.copy(avatar = avatar))
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

性能优化

总结

Kotlin 协程是强大的异步编程工具,但需要正确使用:

参考资源
Kotlin 官方文档:kotlinlang.org/docs/coroutines
Kotlin Coroutines 指南:github.com/Kotlin/kotlinx.coroutines