移动/游戏
Android Kotlin 协程并发问题排查与修复:解决…
当需要审查或修复 Kotlin/Android 代码中的协程并发问题(ANR、内存泄漏、竞态、生命周期崩溃)时,按结构化并发与生命周期安全原则做最小改动修复。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
Android Kotlin 协程并发问通用
解决思路
- 先做问题分诊:记录确切的错误、崩溃或症状(ANR、内存泄漏、竞态条件、状态错误);检查项目的协程依赖版本(kotlinx-coroutines-android、lifecycle-runtime-ktx);确认当前使用的 scope 上下文(viewModelScope、lifecycleScope、自定义 scope 或没有 scope);确认代码是绑定 UI(Dispatchers.Main)还是应在主线程外运行(Dispatchers.IO、Dispatchers.Default);核实 Dispatcher 是否通过注入方式提供以便测试。
- 优先采用最小且安全的修复,保持现有行为不变,同时满足结构化并发与生命周期安全。
- 修复 ANR / 主线程阻塞:把耗时工作移到 withContext(Dispatchers.IO) 或 Dispatchers.Default,确保 suspend 函数是 main-safe 的。
- 修复内存泄漏 / 僵尸协程:用生命周期绑定的 scope(viewModelScope、lifecycleScope 或注入的 applicationScope)替换 GlobalScope。
- 修复生命周期收集问题:用 repeatOnLifecycle(Lifecycle.State.STARTED) 替换已废弃的 launchWhenStarted。
- 修复状态暴露问题:封装 MutableStateFlow / MutableSharedFlow,只对外暴露只读的 StateFlow 或 Flow。
- 修复 CancellationException 被吞掉:确保 catch (e: Exception) 这类通用捕获块重新抛出 CancellationException。
- 修复不可协作的取消:在紧凑循环中加入 ensureActive() 或 yield() 以支持协作式取消。
- 修复回调 API:把监听器转换为 callbackFlow,并用 awaitClose 做正确的清理。
- 修复硬编码 Dispatcher:通过构造函数注入 CoroutineDispatcher 以便测试。
- Dispatcher 注入(可测试性):正确做法是 class UserRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { suspend fun fetchUser() = withContext(ioDispatcher) { ... } };错误做法是直接在方法内硬编码 withContext(Dispatchers.IO)。
- 生命周期感知的收集:正确做法是 viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } };错误做法是使用已废弃且不安全的 lifecycleScope.launchWhenStarted 直接 collect。
- 状态封装:正确做法是 private val _uiState = MutableStateFlow(UiState()) 配合 val uiState: StateFlow<UiState> = _uiState.asStateFlow();错误做法是直接暴露 val uiState = MutableStateFlow(UiState()) 导致可变性泄漏。
- 异常处理:正确做法是 try { doSuspendWork() } catch (e: CancellationException) { throw e } catch (e: Exception) { handleError(e) },必须重新抛出 CancellationException;错误做法是只写 catch (e: Exception) { handleError(e) } 从而吞掉取消异常。
- 协作式取消:正确做法是在紧凑循环中调用 items.forEach { item -> ensureActive(); processItem(item) };错误做法是循环中从不检查取消。
- 回调转换:正确做法是 fun locationUpdates(): Flow<Location> = callbackFlow { val listener = LocationListener { location -> trySend(location) }; locationManager.requestLocationUpdates(listener); awaitClose { locationManager.removeUpdates(listener) } }。
- Scope 选择指南:viewModelScope 用于 ViewModel 操作,随 ViewModel 清除;lifecycleScope 用于 Activity/Fragment 中的 UI 操作,随生命周期宿主销毁;repeatOnLifecycle 用于 UI 中的 Flow 收集,随生命周期状态启动/停止;注入的 applicationScope 用于应用级后台工作,生命周期为整个应用;GlobalScope 永远不要使用,它会破坏结构化并发。
- 测试模式:使用 runTest,配合 StandardTestDispatcher(testScheduler) 创建测试调度器,构造 FakeRepository 与 ViewModel,调用 viewModel.loadData() 后 advanceUntilIdle(),再断言 assertEquals(UiState.Success(data), viewModel.uiState.value)。
适用边界 · 注意事项
- 不适用于非 Kotlin/Android 项目或未使用 Kotlin Coroutines 的代码库。
- 当问题根因不在协程(如纯 Java 线程、RxJava、底层系统问题)时,本做法不适用。
- 不要为了修复并发问题而做大规模重构或改变现有行为,应优先采用最小安全改动。