Android 开发中使用协程常犯的 10 个错误
介绍
作为 Android 开发者,Kotlin 协程已经成为异步编程工具箱中不可或缺的一部分。它们简化了并发任务,使代码更具可读性,避免了早期方法中常见的回调地狱。然而,协程也带来了新的挑战,容易陷入一些常见的错误,导致Bug、崩溃或性能不佳。本文将探讨一些经常犯的协程错误,并提供规避这些错误的指导。
1.阻塞主线程
错误:
在 Main
调度器上运行长时间或阻塞任务,可能会冻结 UI,导致应用无响应 (ANR) 错误。
解决方案:
始终为协程指定合适的调度器:
// 错误示例
GlobalScope.launch {
// 长时间运行的任务
}
// 正确示例
GlobalScope.launch(Dispatchers.IO) {
// 长时间运行的任务
}
使用 Dispatchers.IO
进行 I/O 操作,使用 Dispatchers.Default
进行 CPU 密集型任务,Dispatchers.Main
保留用来更新UI。
2.忽略协程作用域层次结构
错误:
未正确结构化协程作用域,导致未管理的协程超出其预期生命周期,导致内存泄漏或崩溃。
解决方案:
使用结构并发将协程绑定到特定的生命周期:
• 在Activitie 或 Fragment中,使用 lifecycleScope
或 viewLifecycleOwner.lifecycleScope
。
• 在 ViewModel 中,使用 viewModelScope
。
示例:
// 在 ViewModel 中
viewModelScope.launch {
// 协程工作
}
确保了当关联的生命周期被销毁时,协程被适当地取消。
3.错误处理异常传播
错误:
未能正确处理协程中的异常,导致意外崩溃或沉默失败。
解决方案:
在协程中使用 try-catch
处理异常,特别注意检查 CancellationException
(用于表示协程取消,通常应重新抛出以允许协程正确取消)。
示例:
viewModelScope.launch {
try {
// 可能抛出异常的挂起函数
} catch (e: Exception) {
if (e !is CancellationException) {
// 处理异常
} else {
throw e // 重抛
}
}
}
或者使用 CoroutineExceptionHandler
处理未处理的异常:
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
if (throwable !is CancellationException) {
// 处理未处理的异常
}
}
viewModelScope.launch(exceptionHandler) {
// 可能抛出异常的挂起函数
}
4.使用错误的协程构建器
错误:
混淆 launch
和 async
构建器,导致意外行为,如丢失结果或不必要的并发。
解决方案:
使用 launch
不需要结果时,仅为启动协程。使用 async
需要异步计算值时。
示例:
// 需要结果时使用 async
val deferredResult = async {
computeValue()
}
val result = deferredResult.await()
5.过度使用 GlobalScope
错误:
依赖 GlobalScope
启动协程,导致协程运行时间过长且难以管理。
解决方案:
避免使用 GlobalScope
,除非绝对必要。相反,使用适当作用域的结构化并发:
• lifecycleScope
用于 UI 相关组件。
• viewModelScope
用于 ViewModel。
• 使用适当取消的自定义 CoroutineScope
。
6.未考虑线程安全
错误:
在多个协程中访问或修改共享的可变数据而没有适当的同步,导致竞争条件。
解决方案:
使用线程安全的数据结构。使用 Mutex
或 Atomic
类同步访问。将可变状态限制在特定线程或协程中。
示例使用 Mutex
:
val mutex = Mutex()
var sharedResource = 0
coroutineScope.launch {
mutex.withLock {
sharedResource++
}
}
7.忘记取消协程
错误:
在不再需要协程时未能取消协程,可能浪费资源或引发意外副作用。
解决方案:
使用结构化并发,使协程自动取消。在使用自定义作用域时,确保在适当时机取消它们。
示例:
val job = CoroutineScope(Dispatchers.IO).launch {
// 工作
}
// 完成后取消
job.cancel()
8.在协程内部阻塞
错误:
在协程内部使用 Thread.sleep()
或重计算而不切换到合适的调度器,可能阻塞底层线程。
解决方案:
• 避免在协程内部使用阻塞调用。
• 使用挂起函数如 delay()
替代 Thread.sleep()
。
• 将重计算卸载到 Dispatchers.Default
。
示例:
// 错误示例
launch(Dispatchers.IO) {
Thread.sleep(1000)
}
// 正确示例
launch(Dispatchers.IO) {
delay(1000)
}
9.误用 withContext
错误:
错误使用 withContext
,如不必要地嵌套或误解其用途,导致代码难以阅读或效率低下。
解决方案:
• 使用 withContext
切换特定代码块的上下文。
• 没有必要时不要嵌套 withContext
调用。
• 尽量保持 withContext
块简短。
示例:
// 正确使用
val result = withContext(Dispatchers.IO) {
// 执行 I/O 操作
}
10.没有正确测试协程
错误:
未编写正确测试协程代码,或编写的测试无法正确处理协程,导致测试不稳定或不可靠。
解决方案:
• 使用 kotlinx-coroutines-test
的 runBlockingTest
或 runTest
进行协程单元测试。
• 利用 TestCoroutineDispatcher
和 TestCoroutineScope
控制测试中的协程执行。
• 确保在测试有延迟或超时的代码时正确提前时间。
示例:
@Test
fun testCoroutine() = runTest {
val result = mySuspendingFunction()
assertEquals(expectedResult, result)
}
结论
协程功能强大,但伴随着强大功能也带来了责任。了解这些常见错误并了解如何避免它们,能让你编写更高效、更可靠、更易维护的异步代码。记住以下几点:
• 始终选择正确的调度器。
• 绑定协程到适当的生命周期。
• 周全地处理异常。
• 注意协程的作用域和取消。
• 彻底测试协程代码。 遵循这几点,充分发挥 Kotlin 协程的潜力,为应用用户提供更流畅、更响应迅速的体验。