协程

作者: 长点点 | 来源:发表于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