Kotlin协程从初识到精通(一)协程基础用法

634 阅读5分钟

Android中如何正确使用协程(Coroutine)

Kotlin 协程(Coroutine)是一种轻量级的并发编程工具,在 Android 中尤为重要。它简化了异步任务的处理,使得代码更加简洁、可读且避免了回调地狱。在接下来系列的博客中,我们将介绍如何在 Android 中正确使用协程,并深入探讨协程的各种操作符(builders、scope、suspend 函数、launch、async 等)。

一、什么是协程?

协程是 Kotlin 的一种轻量级线程。它们与普通的线程不同,可以在一个线程中启动多个协程,协程之间的切换是非阻塞的。协程的运行效率高,并且能够简化复杂的异步代码流程。

这句话很重要哦:协程本质上是Kotlin语言包装的线程框架

1.1 协程的优势

  • 轻量:与线程相比,协程的创建和切换开销极低。可以在一个线程内启动成千上万个协程。
  • 可取消:协程可以被取消,这是异步任务处理中很重要的特性。
  • 简化异步代码:协程将异步任务转换为顺序代码,避免了复杂的回调逻辑。

二、协程的基本构造器(Builders)

Kotlin 提供了几种主要的协程构造器:launchasyncrunBlocking 等,每个构造器都有其特定的用途。

2.1 launch

launch 是最常用的协程构造器,它会启动一个新的协程并且不会返回任何结果,适用于不需要返回值的异步操作,如在后台执行一些任务。

val job = CoroutineScope(Dispatchers.IO).launch {
    // 在后台线程执行
    performLongRunningTask()
}

特点:

  • 不会返回结果。
  • launch 返回的是一个 Job,可以用来取消任务。

2.2 async

async 用于执行并行任务并返回结果。它和 launch 的区别在于 async 会返回一个 Deferred,这是一个延迟计算的值,通过 await() 可以获取执行结果。

val result = CoroutineScope(Dispatchers.IO).async {
    // 返回计算结果
    performLongRunningCalculation()
}.await()

特点:

  • 返回一个 Deferred 对象,用于异步返回值。
  • 可以在多个 async 中并发执行任务,并通过 await 来等待结果。

2.3 runBlocking

runBlocking 是一个阻塞当前线程的协程构造器,它通常只在测试或者 main 函数中使用,避免在 Android 主线程中使用它。

runBlocking {
    val result = performNetworkRequest()
    println(result)
}

特点:

  • 阻塞当前线程,直到协程完成。
  • 在 Android 中避免使用 runBlocking,因为它会阻塞主线程。

三、协程上下文和调度器(Dispatchers)

协程调度器定义了协程在什么线程中执行。Kotlin 为协程提供了以下几种调度器:

3.1 Dispatchers.Main

Dispatchers.Main 用于更新 UI,因为它运行在 Android 的主线程中。

CoroutineScope(Dispatchers.Main).launch {
    // 在主线程中执行任务
    updateUI()
}

3.2 Dispatchers.IO

Dispatchers.IO 专门用于 I/O 操作,如网络请求、数据库查询等,这些操作通常需要在后台线程中执行。

CoroutineScope(Dispatchers.IO).launch {
    // 执行网络请求或数据库操作
    val result = performNetworkRequest()
}

3.3 Dispatchers.Default

Dispatchers.Default 适用于 CPU 密集型任务,例如复杂计算等。

CoroutineScope(Dispatchers.Default).launch {
    // 执行密集型计算
    val result = performHeavyCalculation()
}

3.4 Dispatchers.Unconfined

Dispatchers.Unconfined 不指定任何线程,它会在调用它的线程上启动协程,但只会持续到第一个挂起点。

CoroutineScope(Dispatchers.Unconfined).launch {
    // 随着挂起函数的切换,可能会在不同线程执行
    log("Start")
    delay(1000)
    log("End")
}

四、挂起函数(Suspend Functions)

挂起函数(suspend)是协程中最重要的概念之一。它们可以在协程中被调用,并在必要时“挂起”协程而不阻塞线程。当挂起的操作完成后,协程将继续执行。

suspend fun performNetworkRequest(): String {
    delay(1000) // 模拟网络请求延迟
    return "Request Result"
}

特点:

  • 只能在协程或者另一个挂起函数中调用。
  • 允许执行长时间运行的任务而不阻塞主线程。

五、协程作用域(Scope)和 Job 取消操作

协程作用域(CoroutineScope)用于控制协程的生命周期。Android 中常用的协程作用域有 GlobalScope 和基于 ViewModelviewModelScope

5.1 GlobalScope

GlobalScope 会启动一个顶层协程,它的生命周期和整个应用一样长,容易引发内存泄漏,因此不推荐使用。

GlobalScope.launch {
    // 执行长期后台任务
}

5.2 viewModelScope

viewModelScope 是 Android 提供的一个用于 ViewModel 的协程作用域,协程会在 ViewModel 销毁时自动取消,非常适合执行与 UI 相关的任务。

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            val result = performNetworkRequest()
            // 更新 LiveData 或 State
        }
    }
}

5.3 Job 的取消与超时

协程是可以被取消的。每个 launchasync 返回的都是一个 Job,可以调用 job.cancel() 取消协程。

val job = CoroutineScope(Dispatchers.IO).launch {
    performNetworkRequest()
}
job.cancel() // 取消协程

协程还可以设置超时时间,使用 withTimeout 来取消超时任务:

try {
    withTimeout(5000) { // 设置5秒超时
        performNetworkRequest()
    }
} catch (e: TimeoutCancellationException) {
    println("任务超时")
}

六、协程的其他操作符

6.1 withContext

withContext 用于切换协程上下文,它可以在协程中从一个调度器切换到另一个调度器。

suspend fun performTask() {
    withContext(Dispatchers.IO) {
        // 在后台线程执行
    }
    withContext(Dispatchers.Main) {
        // 回到主线程更新UI
    }
}

6.2 repeat

repeat 用于在协程中多次重复执行一个任务。

launch {
    repeat(5) {
        delay(1000)
        println("Repeated $it")
    }
}

七、正确使用协程的最佳实践

  1. 避免在 ActivityFragment 中使用 GlobalScope:使用 GlobalScope 启动的协程生命周期与应用一样长,容易引发内存泄漏。推荐使用 lifecycleScopeviewModelScope

  2. 确保协程的取消机制:在涉及长时间运行任务的场景中,确保正确地处理协程的取消,以避免不必要的资源占用。

  3. 合理使用调度器:网络请求和数据库操作应该使用 Dispatchers.IO,UI 操作应使用 Dispatchers.Main,而密集计算则应使用 Dispatchers.Default

  4. 使用 suspend 函数简化异步代码:将复杂的异步操作封装在挂起函数中,确保代码的可读性和简洁性。

八、总结

通过合理地使用协程构造器、调度器、作用域以及取消机制,我们可以编写出更简洁、可维护的代码。掌握协程的各类操作符并将它们应用在实际项目中,你将会显著提高代码的可读性和效率。