Skip to content

协程最佳实践与常见误区

基于 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 是危险的。 请始终通过 repeatOnLifecycleflowWithLifecycle 进行收集,以避免后台资源浪费。

MutableStateFlow 是线程安全的,MutableList 不是

不要在协程中并发读写普通的 ArrayListHashMap

  • 简单方案: 使用 Mutex 保护。
  • 高性能方案: 使用 ConcurrentHashMapAtomic 类。
  • 状态方案: 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() 
        }
    }
}