协程最佳实践与常见误区
基于 Google 官方建议与业界实战总结,以下是 Kotlin 协程开发的核心原则。
注入调度器,不要硬编码
不推荐写法:
kotlin
launch(Dispatchers.IO) { ... } // ❌ 难以测试最佳实践: 通过构造函数注入 Dispatcher,便于单元测试时替换为 StandardTestDispatcher。
kotlin
class Repository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun load() = withContext(ioDispatcher) { ... }
}挂起函数应该是“主线程安全”的
原则: 任何 suspend 函数都不应该阻塞调用线程。如果涉及耗时操作,必须在函数内部使用 withContext 切换线程。
kotlin
// ✅ 安全:调用者在 Main 线程调用也不会卡顿
suspend fun saveFile() = withContext(Dispatchers.IO) {
file.writeText(...)
}严禁使用 GlobalScope
GlobalScope 主要用于“即便应用组件部分销毁也要继续执行”的特殊任务。但在 Android 中,这意味着内存泄漏。
- 替代: 使用
viewModelScope,lifecycleScope或自定义管理的CoroutineScope。
不要捕获 CancellationException
协程的取消机制依赖于 CancellationException 的抛出和传播。如果你捕获并吞掉了它,协程将无法正常取消。
kotlin
try {
delay(1000)
} catch (e: Exception) {
// ❌ 错误:吞掉了 CancellationException
// 如果必须捕获,请重新抛出
if (e is CancellationException) throw e
logError(e)
}Flow 收集要感知生命周期
在 Android UI 中直接 collect 是危险的。 请始终通过 repeatOnLifecycle 或 flowWithLifecycle 进行收集,以避免后台资源浪费。
MutableStateFlow 是线程安全的,MutableList 不是
不要在协程中并发读写普通的 ArrayList 或 HashMap。
- 简单方案: 使用
Mutex保护。 - 高性能方案: 使用
ConcurrentHashMap或Atomic类。 - 状态方案:
StateFlow.update { ... }是原子的,推荐用于状态管理。
避免在 try-catch 块中直接 emit
为了保持异常透明性,不要包裹 emit。请使用 .catch {} 操作符。
ApplicationScope 的正确姿势
如果你需要执行一个“跨越界面生命周期”的任务(如上传日志),请不要用 GlobalScope。建立一个受控的全局作用域。
kotlin
// 定义全局作用域
class ApplicationScope : CoroutineScope {
// 1. SupervisorJob: 确保一个子协程失败不会导致整个 Scope 取消
// 2. Default: 默认运行在后台线程
// 3. ExceptionHandler: 捕获全局未处理异常
override val coroutineContext = SupervisorJob() +
Dispatchers.Default +
CoroutineExceptionHandler { _, e -> Log.e("AppScope", "Error", e) }
}
// 在 Application 中初始化 (或者使用依赖注入框架单例绑定)
class MyApplication : Application() {
// 全局单例
companion object {
val scope = ApplicationScope()
}
}
// 使用场景
class MyViewModel : ViewModel() {
fun uploadLog() {
// 即使 ViewModel 销毁,这个任务也会在全局 Scope 中继续运行
MyApplication.scope.launch {
api.upload()
}
}
}