首页 Android 正文
  • 本文约2691字,阅读需13分钟
  • 94
  • 0

Android 开发中使用协程常犯的 10 个错误

摘要

介绍 作为 Android 开发者,Kotlin 协程已经成为异步编程工具箱中不可或缺的一部分。它们简化了并发任务,使代码更具可读性,避免了早期方法中常见的回调地狱。然而,协程也带来了新的挑战,容易陷入一些常见的错误,导致Bug、崩溃或性能不佳。本文将探讨一些经常犯的协程错误,并提供规避这些错误的指导。 阻塞主线程 错误: 在 Main 调度器上运行长时间或...

介绍

作为 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中,使用 lifecycleScopeviewLifecycleOwner.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.使用错误的协程构建器

错误:

混淆 launchasync 构建器,导致意外行为,如丢失结果或不必要的并发。

解决方案:

使用 launch 不需要结果时,仅为启动协程。使用 async 需要异步计算值时。

示例:

// 需要结果时使用 async
val deferredResult = async {
    computeValue()
}
val result = deferredResult.await()

5.过度使用 GlobalScope

错误:

依赖 GlobalScope 启动协程,导致协程运行时间过长且难以管理。

解决方案:

避免使用 GlobalScope,除非绝对必要。相反,使用适当作用域的结构化并发:

lifecycleScope 用于 UI 相关组件。

viewModelScope 用于 ViewModel。

• 使用适当取消的自定义 CoroutineScope

6.未考虑线程安全

错误:

在多个协程中访问或修改共享的可变数据而没有适当的同步,导致竞争条件。

解决方案:

使用线程安全的数据结构。使用 MutexAtomic 类同步访问。将可变状态限制在特定线程或协程中。

示例使用 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-testrunBlockingTestrunTest 进行协程单元测试。

• 利用 TestCoroutineDispatcherTestCoroutineScope 控制测试中的协程执行。

• 确保在测试有延迟或超时的代码时正确提前时间。

示例:

@Test
fun testCoroutine() = runTest {
    val result = mySuspendingFunction()
    assertEquals(expectedResult, result)
}

结论

协程功能强大,但伴随着强大功能也带来了责任。了解这些常见错误并了解如何避免它们,能让你编写更高效、更可靠、更易维护的异步代码。记住以下几点:

• 始终选择正确的调度器。

• 绑定协程到适当的生命周期。

• 周全地处理异常。

• 注意协程的作用域和取消。

• 彻底测试协程代码。 遵循这几点,充分发挥 Kotlin 协程的潜力,为应用用户提供更流畅、更响应迅速的体验。

标签:协程

扫描二维码,在手机上阅读


    评论