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
协程作用域,用于管理协程的生命周期:
| 作用域 | 使用场景 | 生命周期 |
|---|---|---|
| viewModelScope | Android ViewModel | ViewModel 销毁时自动取消 |
| lifecycleScope | Activity/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-catch 或 CoroutineExceptionHandler
陷阱 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")
}
}
}
}
性能优化
- 避免过度使用 async:只有需要并行执行时才用 async,否则用 launch
- 合理选择 Dispatcher:IO 操作别在 Main 线程,CPU 计算别在 IO 线程
- 使用 Flow 替代回调:响应式数据流更适合协程
- 避免协程嵌套:不要在协程中再启动协程
总结
Kotlin 协程是强大的异步编程工具,但需要正确使用:
- ✅ 优先使用挂起函数而非 Deferred
- ✅ 使用结构化并发管理生命周期
- ✅ 在作用域边界处理异常
- ✅ 合理选择 Dispatcher
- ✅ 避免 GlobalScope 和线程阻塞
参考资源:
Kotlin 官方文档:kotlinlang.org/docs/coroutines
Kotlin Coroutines 指南:github.com/Kotlin/kotlinx.coroutines
Kotlin 官方文档:kotlinlang.org/docs/coroutines
Kotlin Coroutines 指南:github.com/Kotlin/kotlinx.coroutines