协程

作者: 长点点 | 来源:发表于2023-05-08 15:06 被阅读0次

安卓开发中kotlin中的协程

一、什么是协程

协程(coroutine)是一种编写异步代码的方式,它可以让我们用同步的方式来写异步的代码,避免了回调地狱和复杂的线程管理。协程可以在不阻塞主线程的情况下,执行一些耗时的任务,例如网络请求、数据库操作或文件读写等。

协程的核心概念有以下几个:

  • 挂起函数(suspend function):一种可以暂停和恢复执行的特殊函数,它只能在协程或者其他挂起函数中调用。挂起函数可以让协程在等待某个结果的时候,释放当前线程,让其他协程继续执行,从而提高并发效率。
  • 协程作用域(coroutine scope):一种定义协程生命周期范围的对象,它可以管理协程的启动、取消和结构化并发。每个协程都必须在某个作用域内运行,当作用域被销毁时,它内部的所有协程都会被自动取消。
  • 协程构建器(coroutine builder):一种用于创建和启动协程的函数,它接收一个挂起函数作为参数,并返回一个协程对象。常见的协程构建器有launchasync两种,它们分别用于执行不返回结果和返回结果的异步任务。
  • 调度器(dispatcher):一种决定协程在哪个线程上执行的对象,它可以指定协程运行的线程池或者线程模式。Kotlin提供了三种调度器,分别是Dispatchers.MainDispatchers.IODispatchers.Default,它们分别适用于在主线程、后台线程执行I/O操作和后台线程执行CPU密集型操作的场景。
  • 上下文(context):一种包含了协程相关信息的对象,例如调度器、作业、名称等。每个协程都有自己的上下文,可以通过coroutineContext属性访问。上下文可以在创建协程时指定,也可以在运行时切换。

二、如何使用协程

要在安卓开发中使用协程,我们需要添加以下依赖项:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
}

这些依赖项包含了kotlin协程的核心库和安卓专用的扩展函数。

接下来,我们看一些使用协程的示例:

1. 启动一个简单的协程

要启动一个协程,我们需要使用一个协程构建器,例如launchasync。协程构建器是一种特殊的函数,它可以在某个作用域内创建和启动一个新的协程。作用域是一种控制结构,它可以定义协程的生命周期和上下文。

以下是一个使用launch协程构建器在全局作用域内启动一个简单的协程的示例:

fun main() {
    // 在全局作用域内启动一个新的协程
    GlobalScope.launch {
        // 在协程中执行一些耗时操作
        delay(1000) // 模拟网络请求
        println("Hello from coroutine") // 在控制台打印结果
    }
    // 在主线程中继续执行其他任务
    println("Hello from main thread") // 在控制台打印结果
    Thread.sleep(2000) // 阻塞主线程,等待协程完成
}

输出:

Hello from main thread
Hello from coroutine

在上面的示例中,我们使用GlobalScope.launch在全局作用域内启动了一个新的协程。全局作用域是一种预定义的顶层作用域,它可以让我们在应用程序的整个生命周期内创建和运行协程。我们可以给launch传入一个代码块,它会在协程中执行。在代码块中,我们使用了delay函数来模拟一个耗时的网络请求,然后打印了结果。注意,delay是一种特殊的挂起函数,它可以让当前的协程暂停一段时间,而不会阻塞当前的线程。挂起函数是一种只能在协程或其他挂起函数中调用的函数,它可以让协程在等待结果时释放线程资源,以供其他协程或任务使用。

同时,在主线程中,我们也打印了结果,并使用了Thread.sleep函数来阻塞主线程,等待协程完成。注意,这里我们使用了Thread.sleep而不是delay,因为delay只能在协程或挂起函数中使用,而主函数不是一个挂起函数。另外,这里我们使用了Thread.sleep只是为了演示效果,实际开发中不推荐这样做,因为这样会浪费线程资源,并且可能导致应用程序无响应。

从输出结果可以看出,主线程和协程是并发执行的,而不是顺序执行的。这意味着主线程不会等待协程完成就继续执行其他任务。如果我们想要让主线程等待协程完成再执行其他任务,我们可以使用一些同步机制,例如使用joinawait函数来等待协程的结束或结果。

2. 在生命周期感知型组件中使用协程

在安卓开发中,我们通常需要根据组件的生命周期来管理协程的启动和取消,以避免内存泄漏或无用的工作。例如,我们可能只想在Activity或Fragment处于活动状态时才执行某些协程任务,而在它们被销毁时取消这些任务。

为了方便我们在生命周期感知型组件中使用协程,Kotlin提供了一些内置的协程作用域和扩展函数,它们可以让我们根据组件的生命周期自动启动和取消协程。这些作用域和函数包含在KTX扩展中,我们需要添加以下依赖项:

dependencies {
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0'
}

以下是一些常用的生命周期感知型协程作用域和函数:

  • ViewModelScope:每个ViewModel都定义了一个ViewModelScope,它会在ViewModel被清除时自动取消所有的协程。这个作用域适合一些与界面相关的协程任务,例如计算数据或更新LiveData等。我们可以通过viewModelScope属性访问ViewModel的CoroutineScope,如下所示:
class MyViewModel : ViewModel() {
    init {
        viewModelScope.launch {
            // 在ViewModelScope中启动一个协程
        }
    }
}
  • LifecycleScope:每个Lifecycle对象都定义了一个LifecycleScope,它会在Lifecycle被销毁时自动取消所有的协程。这个作用域适合一些与界面交互相关的协程任务,例如更新视图或动画等。我们可以通过lifecycle.coroutineScopelifecycleOwner.lifecycleScope属性访问Lifecycle的CoroutineScope,如下所示:
class MyFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            // 在LifecycleScope中启动一个协程
        }
    }
}
  • repeatOnLifecycle:这是一个挂起函数,它可以让我们在Lifecycle处于某个特定状态时执行一个代码块,并在它处于其他状态时取消执行。这个函数适合一些需要根据界面可见性来控制执行的协程任务,例如收集数据流或播放音频等。我们可以指定Lifecycle的最低状态(如STARTED或RESUMED),并传入一个挂起函数作为参数,如下所示:
class MyFragment : Fragment() {
    val viewModel: MyViewModel by viewModel()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle会在每次Lifecycle达到STARTED状态时启动一个新的协程,并在STOPPED状态时取消它
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // 在这个挂起函数中执行需要重启的协程任务
                viewModel.someDataFlow.collect { 
                    // 处理数据流
                }
            }
        }
    }
}
  • flowWithLifecycle:这是一个扩展函数,它可以让我们对一个数据流进行生命周期感知型收集,即只有当Lifecycle处于某个特定状态时才收集数据流,并在其他状态时停止收集。这个函数适合一些需要根据界面

可见性来控制收集的协程任务,例如收集数据流或播放音频等。我们可以指定Lifecycle的最低状态(如STARTED或RESUMED),并传入一个数据流作为参数,如下所示:

viewLifecycleOwner.lifecycleScope.launch {
    // flowWithLifecycle会在Lifecycle达到STARTED状态时开始收集数据流,并在STOPPED状态时停止收集
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect { 
            // 处理数据流
        }
}

3. 在协程中返回结果

有时候,我们需要在协程中执行一些返回结果的异步任务,例如从网络或数据库中获取数据等。这时候,我们可以使用async协程构建器来创建一个返回Deferred对象的协程,然后使用await挂起函数来获取结果。asyncawait的组合类似于Java中的Futureget

以下是一个使用asyncawait的示例:

fun main() = runBlocking {
    // 在主协程中启动两个子协程
    val deferred1 = async { getData1() } // 返回Deferred<String>
    val deferred2 = async { getData2() } // 返回Deferred<String>
    // 等待两个子协程都完成,并获取它们的结果
    val result1 = deferred1.await() // 挂起并获取String
    val result2 = deferred2.await() // 挂起并获取String
    // 在主协程中处理结果
    println("result1: $result1")
    println("result2: $result2")
}

suspend fun getData1(): String {
    delay(1000) // 模拟耗时操作
    return "Hello"
}

suspend fun getData2(): String {
    delay(2000) // 模拟耗时操作
    return "World"
}

输出:

result1: Hello
result2: World

注意,两个子协程是并发执行的,而不是顺序执行的。这意味着总共只花了大约2秒钟,而不是3秒钟。如果我们想要顺序执行两个子协程,我们可以直接在主协程中调用挂起函数,而不需要使用asyncawait,如下所示:

fun main() = runBlocking {
    // 在主协程中顺序调用两个挂起函数
    val result1 = getData1() // 挂起并获取String
    val result2 = getData2() // 挂起并获取String
    // 在主协程中处理结果
    println("result1: $result1")
    println("result2: $result2")
}

suspend fun getData1(): String {
    delay(1000) // 模拟耗时操作
    return "Hello"
}

suspend fun getData2(): String {
    delay(2000) // 模拟耗时操作
    return "World"
}

输出:

result1: Hello
result2: World

这次,两个子协程是顺序执行的,总共花了大约3秒钟。

三、协程的优缺点

协程相比于传统的异步编程方式,有以下一些优点:

  • 简洁:协程可以让我们用同步的方式来写异步的代码,避免了回调地狱和复杂的线程管理。我们可以使用挂起函数、作用域、调度器等工具来简化异步编程的逻辑和流程。
  • 高效:协程可以在不阻塞主线程上执行任务,而不会阻塞或占用线程。协程可以在需要时创建大量的协程,而不会像线程那样消耗系统资源。协程还可以在不同的线程之间灵活地切换,以实现最佳的性能和用户体验。
  • 安全:协程可以避免一些常见的并发问题,例如竞态条件、死锁、内存泄漏等。协程可以使用作用域、调度器、上下文等工具来控制并发的范围、线程和生命周期。协程还可以利用Kotlin的空安全和异常处理机制来提高代码的健壮性。

当然,协程也有一些缺点,例如:

  • 兼容性:协程是Kotlin语言的一个特性,如果要在Java中使用协程,就需要借助一些桥接库或工具,例如kotlinx-coroutines-jdk8或kotlinx-coroutines-rx2等。这些库或工具可能会增加项目的复杂度和维护成本,也可能会引入一些潜在的问题或风险。
  • 调试:协程的调试可能比较困难,因为协程可以在不同的线程之间切换,而且协程的堆栈跟踪可能不太完整或清晰。开发者需要使用一些专门的工具或技巧来调试协程,例如使用CoroutineName上下文元素来给协程命名,或使用DebugProbes来检查协程的状态和堆栈跟踪等。

相关文章

网友评论

      本文标题:协程

      本文链接:https://www.haomeiwen.com/subject/srdlsdtx.html