谈谈我对kotlin中协程的理解(代码片段)

小羊子说 小羊子说     2022-11-30     732

关键词:

文章目录

1 协程(Coroutines)是什么

kotlin 官方文档说:本质上,协程是轻量级的线程。

从面试角度考查对协程的了解:(2020.8.7 新增)
协程是轻量级的线程,为什么是轻量的?可以先告诉大家结论,因为它基于线程池API,所以在处理并发任务这件事上它真的游刃有余。
有可能有的同学问了,既然它基于线程池,那我直接使用线程池或者使用 Android 中其他的异步任务解决方式,比如 Handler、RxJava等,不更好吗?
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱,这是其最大的优点。

从 Android 开发者的角度去理解它们的关系:

  • 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
  • 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
  • 单线程中的协程总的执行时间并不会比不用协程少。
  • Android 系统上,如果在主线程进行网络请求,会抛出 NetworkOnMainThreadException,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。

我们学习Kotlin 中的协程,一开始确实可以从线程控制的角度来切入。因为在 Kotlin 中,协程的一个典型的使用场景就是线程控制。就像 Java 中的 Executor 和 Android 中的 AsyncTaskKotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。

小结:

  • 协程最常用的功能是并发,而并发的典型场景就是多线程。

  • 协程设计的初衷是为了解决并发问题,让 协作式多任务实现起来更加方便。

  • 简单理解 Kotlin 协程的话,就是封装好的线程池,也可以理解成一个线程框架。

  • 那么Kotlin中的协程是通过什么来实现异步操作的呢?它使用的是一种叫做 挂起 的机制。

协程的开发人员 Roman Elizarov 是这样描述协程的
协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 – 协程挂起。

挂起函数挂起协程时,不会阻塞协程所在的线程。(2020.8.7新增)

在了解协程时,不同的解读,看到不同的定义,我们需要多次去解读,结合应用场景去深入理解。

2 你需要用协程吗?

RxJava 可以解决回调问题,同样我们可以用协程解决回调问题。

3 使用协程优点

  • 轻量级,占用更少的系统资源;

  • 更高的执行效率;

  • 挂起函数较于实现Runnable或Callable接口更加方便可控;

  • kotlin.coroutine 核心库的支持,让编写异步代码更加简单。

4 kotlin协程的演进


解释说明:

  • Job: 任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期
  • Coroutine context:协程上下文,协程上下文里是各种元素的集合
  • Coroutine dispatchers :协程调度,可以指定协程运行在 Android 的哪个线程里
  • suspend:挂起函数。挂起,就是一个稍后会被自动切回来的线程调度操作。

5 实现方式

5.1 环境准备

  • Kotlin 版本: 1.3.+
  • 依赖的框架:在 app/build.gradle 里添加 Kotlin 协程库的依赖如下所示。
//kotlin 标准库
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

//依赖协程核心库 ,提供Android UI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"

//依赖当前平台所对应的平台库 (必须)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
    

5.2 创建协程的几种方式

方式作用
launch:job创建一个不会阻塞当前线程、没有返回结果的 Coroutine,但会返回一个 Job 对象,可以用于控制这个 Coroutine 的执行和取消,返回值为Job。
runBlocking:T创建一个会阻塞当前线程的Coroutine,常用于单元测试的场景,开发中一般不会用到
async/await:Deferredasync 返回的 Coroutine 多实现了 Deferred 接口,简单理解为带返回值的launch函数

实现方式一:GlobalScope.launch,使用 GlobalScope 单例对象, 可以直接调用 launch 开启协程。

  override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_thread)
        loadData()
    

	private fun loadData() 
        GlobalScope.launch(Dispatchers.IO)  //在IO线程开始
            //IO 线程里拉取数据
            val result = fetchData()
            //主线程里更新 UI
            withContext(Dispatchers.Main)  //执行结束后,自动切换到UI线程
                tvShowContent.text = result
            
        
    
	
	//关键词 suspend
    private suspend fun fetchData(): String 
        delay(2000) // delaying for 2 seconds to keep JVM alive
        return "content"
    

我们最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。 该方式启动的协程任务是不会阻塞线程的*

实现方式二:使用 runBlocking 顶层函数

runBlocking 是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的 。

fun main(args: Array<String>) = runBlocking  // start main coroutine
    launch  // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive

实现方式三:async+await

    private fun testAysnc() = GlobalScope.launch 
        val deferred = async(Dispatchers.IO) 
            delay(3000L)
            "Show Time"
        
        // 此处获取耗时任务的结果,我们挂起当前协程,并等待结果
        val result = deferred.await()

        //挂起协程切换至UI线程 展示结果
        withContext(Dispatchers.Main) 
            tvShowContent.text = result
        
    
  • async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。

  • async用于启动一个异步的协程任务,await用于去得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。

那我们平日里常用到的调度器有哪些?

Dispatchers种类作用
Dispatchers.Default共享后台线程池里的线程(适合 CPU 密集型的任务,比如计算)
Dispatchers.MainAndroid中的主线程
Dispatchers.IO共享后台线程池里的线程(针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求)
Dispatchers.Unconfined不限制,使用父Coroutine的现场

回到我们的协程,它从 suspend 函数开始脱离启动它的线程,继续执行在 Dispatchers 所指定的 IO 线程。

紧接着在 suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来

这个"切回来"是什么意思?

我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;

当这个函数执行完毕后,线程又切了回来,"切回来"也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继续回到主线程去执行。

6 协程的应用场景

6.1 从相册中读取图片并显示

从相册中直接读取图片,这是一个典型的IO操作使用场景,操作不当,可能会出现ANR。

版本1.0实现方式

val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, mImageUri)
imageView.setImageBitmap(bitmap)

版本2.0 我们可能会引入HandlerAysnTask来通过异步的方式实现

版本3.0 我们可以这样用doAsync实现 这种方式也不错

doAsync
    //后台执行
   val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
   val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mImageUri) 
    
    //回到主线程
    uiThread
       imageView.setImageBitmap(bitmap)
    

版本4.0 时我们就可以用协程来实现。


val job = launch(Background) 
   val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
   val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mImageUri) 
   launch(UI) 
    imageView.setImageBitmap(bitmap)
  

这里的参数Background是一个CoroutineContext对象,确保这个协程运行在一个后台线程,确保你的应用程序不会因耗时操作而阻塞和崩溃。你可以像下边这样定义一个CoroutineContext:

internal val Background = newFixedThreadPoolContext(2, "bg")

人个感觉 最后两种方式都可取。

6.2 Android Jetpack 中使用 kotlin 协程

后面介绍的三种使用方式在实现前需要分别添加以下的依赖包

    implementation  'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc02'

    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc02'

6.2.1在ViewModel中使用ViewModelScope

为应用程序中的每个ViewModel定义ViewModelScope。如果清除ViewModel,则在此作用域中启动的任何协同程序都将自动取消。

当只有在ViewModel处于活动状态时才需要完成工作时,协程在这里非常有用。

例如,如果要为布局计算某些数据,则应将工作范围设置为ViewModel,以便在清除ViewModel时,自动取消工作以避免消耗资源。

可以通过ViewModel的viewModelScope属性访问ViewModel的协同作用域,如下例所示:


class MyViewModel :ViewModel() 
    init 
        viewModelScope.launch 
            // Coroutine that will be canceled when the ViewModel is cleared.
        
    

6.2.2 在Activity或Fragment中使用LifecycleScope

为每个Lifecycle定义LifecycleScope。当 Lifecycle 销毁时,在此范围内启动的任何协同程序都将被取消。

您可以通过Lifecycle.CoroutineScopelifecycleOwner.lifecycleScope属性访问LifecycleCoroutineScope

下面的示例演示如何使用lifecycleOwner.lifecycleScope异步创建预计算文本:

class MyFragment: Fragment() 
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) 
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch 
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) 
                PrecomputedTextCompat.create(longTextContent, params)
            
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        
    

6.2.3 使用LiveData

使用LiveData时,可能需要异步计算值。例如,您可能希望检索用户的首选项并将其提供给您的UI。在这些情况下,可以使用liveData builder函数调用suspend函数,将结果作为liveData对象提供。

在下面的示例中,loadUser()是在别处声明的挂起函数。使用liveData 构建函数异步调用loadUser(),然后使用emit()发出结果。

val user: LiveData<User> = liveData 
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)

LiveData构建块充当协同路由和liveData之间的结构化并发原语。代码块在LiveData变为活动时开始执行,并且在LiveData变为非活动时经过可配置的超时后自动取消。如果在完成之前取消,则在LiveData再次激活时重新启动。如果在上一次运行中成功完成,则不会重新启动。请注意,只有在自动取消时才会重新启动。如果由于任何其他原因(例如抛出异常CancelationException)而取消块,则不会重新启动它。

也可以从块中发射多个值。每次emit()调用都会暂停块的执行,直到在主线程上设置LiveData值。

val user: LiveData<Result> = liveData 
    emit(Result.loading())
    try 
        emit(Result.success(fetchUser()))
     catch(ioException: Exception) 
        emit(Result.error(ioException))
    

我们也可以和 LifeCycle中的Transformations结合使用,如下例所示:

class MyViewModel: ViewModel() 
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap  id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) 
            emit(database.loadUserById(id))
        
    

6.3 在Retofit 使用kotlin协程

retrofit 2.6.0(2019-06-05)中的更新日志如下:

Support suspend modifier on functions for Kotlin! This allows you to express the asynchrony of HTTP requests in an idiomatic fashion for the language.

@GET("users/id")
suspend fun user(@Path("id") id: Long): User

Behind the scenes this behaves as if defined as fun user(...): Call and then invoked with Call.enqueue. You can also return Response for access to the response metadata.

在函数前加上 suspend 函数直接返回你需要对象类型不需要返回Call对象

总结

本文总结了kotlin中的协程的相关知识点,协程是值得深入研究的。 未来的项目中运用是趋势所在,现将学习的心得总结于此,方便未来迭代中做为技术的储备。如有不足之处,欢迎留言讨论。

参考资料:

1.Google官网在component中协程的运用

2.小慕带你学习Kotlin之协程

3.Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的

4.Kotlin协程的使用

5.【码上开学】Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了

6.kotlin 协程在 Android 中的使用(Jetpack 中的协程、Retofit中使用协程)

7.Kotlin协程 —— 今天说说 launch 与 async
8.即学即用Kotlin - 协程(结合RxJava和不同角度对协程的解读,总结的系列不错)(2020.8.7新增)
9.Kotlin Coroutines(协程) 完全解析(一),协程简介
(介绍了挂起函数的运用场景,值得一读)(2020.8.7新增)

深入理解kotlin协程协程的创建启动挂起函数理论篇(代码片段)

kotlin实现恢复挂起点是通过一个接口类Continuation(英文翻译过来叫"继续、延续、续体")来实现的。Kotlin续体有两个接口: Continuation 和 CancellableContinuation,顾名思义 CancellableContinuation 是一个可以取消的 Continuation... 查看详情

深入理解kotlin协程使用job控制协程的生命周期(代码片段)

Job 是协程上下文CoroutineContext的实现之一,通过它我们可以对协程的生命周期进行一些控制操作。       Job是协程的句柄。使用 launch 或 async 创建的每个协程都会返回一个 Job 实例对象,该实例是相应协程的唯一... 查看详情

swoole中协程的使用注意事项及协程中的异常捕获(代码片段)

协程使用注意事项协程内部禁止使用全局变量,以免发生数据错乱;协程使用use关键字引入外部变量到当前作用域禁止使用引用,以免发生数据错乱;不能使用类静态变量Class::$array/全局变量$_array/全局对象属性$object->array/其... 查看详情

深入kotlin-协程的取消(代码片段)

取消funmain()=runBlockingvaljob=GlobalScope.launchrepeat(200)println(it)delay( 查看详情

深入kotlin-协程的取消(代码片段)

取消funmain()=runBlockingvaljob=GlobalScope.launchrepeat(200)println(it)delay( 查看详情

kotlin协程的前世今生(代码片段)

Kotlin协程的前世今生CoroutineScope协程作用域CoroutineContext协程上下文Job组合上下文中的元素调度器协程取消和超时协程异常协程中有几个概念:CoroutineScope,Job,CoroutineContextCoroutineScope协程作用域异步作用域函数创建作用域有两种创建... 查看详情

kotlin协程基础概念深入理解(代码片段)

本文需要读者对协程有基础的了解,关于协程的使用,可以参考官方教程:[[play.kotlinlang.org/hands-on/In…](https://link.juejin.cn?target=https%3A%2F%2Fplay.kotlinlang.org%2Fhands-on%2FIntroduction%2520to%2520Corou 查看详情

kotlin协程协程的挂起和恢复①(协程的挂起和恢复概念|协程的suspend挂起函数)(代码片段)

文章目录一、协程的挂起和恢复概念二、协程的suspend挂起函数一、协程的挂起和恢复概念函数最基本的操作是:调用call:通过函数名或函数地址调用函数;返回return:函数执行完毕后,继续执行函数调用的下一行代码;协程在调用call和... 查看详情

kotlin协程协程的挂起和恢复①(协程的挂起和恢复概念|协程的suspend挂起函数)(代码片段)

文章目录一、协程的挂起和恢复概念二、协程的suspend挂起函数一、协程的挂起和恢复概念函数最基本的操作是:调用call:通过函数名或函数地址调用函数;返回return:函数执行完毕后,继续执行函数调用的下一行代码;协程在调用call和... 查看详情

lua中协程的使用

   平常访问网络都会使用回调的方式,现在通过协程改变这种回调的模式,让异步方法按同步的方法来使用--co.luayield=coroutine.yieldco=function(func,cb)localcor=coroutine.create(func)localnext=coroutine.resumelocalhasNext;hasNext=function( 查看详情

kotlin协程源码分析-协程的启动(代码片段)

1.简单启动协程Demo本文例子代码地址:gitee.com/mcaotuman/k…写个小demo,先记住怎么用,后面再分析源码。三个挂起函数:输出结果:2.构建suspend修饰的lambda函数2.1suspendlambda下面这段代码是suspend修饰lambda表达式:v... 查看详情

kotlin协程的四种启动模式(代码片段)

Default:协程创建后立即开始调度。在调度前如果协程被取消。将其直接进入取消相应的状态。ATOMIC:协程创建后。立即开始调度。协程执行到第一个挂起点之前不相应取消。LAZY:只有协程被需要时,包括主动调协程的... 查看详情

kotlin协程硬核解读(4.协程的创建和启动流程分析)(代码片段)

...utineScope协程作用域1.2AbstractCoroutine协程1.3Continuation续体1.4kotlin扩展函数1.5kotlin的函数类型Function1.6调用操作符()重载2.协程的启动、执行流程分析2.1CoroutineScope.launch()创建协程对象2.2initParentJob()父子Job绑定,传递取消2.3CoroutineSt... 查看详情

kotlin协程硬核解读(4.协程的创建和启动流程分析)(代码片段)

...utineScope协程作用域1.2AbstractCoroutine协程1.3Continuation续体1.4kotlin扩展函数1.5kotlin的函数类型Function1.6调用操作符()重载2.协程的启动、执行流程分析2.1CoroutineScope.launch()创建协程对象2.2initParentJob()父子Job绑定,传递取消2.3CoroutineSt... 查看详情

kotlin协程的迷惑(代码片段)

Kotlin作为谷歌强力推广的Android编程语言,是运行在jvm上的,在很多地方可以看做java语言披了一层语法糖,很多地方其实和java是相通的,包括各种库的使用。不过倒是有一个新东西:协程,这个对于java程... 查看详情

用chatgpt帮你解答kotlin协程的原理级面试常见题(代码片段)

kotlin协程原理问答首先我问了它一个关于Kotlin协程的原理:问:介绍一下kotlin协程的原理Kotlin协程是Kotlin语言中的一种非阻塞的、顺序执行的编程构造,可以在不阻塞线程的情况下执行异步任务。它与Java中的线程和Ja... 查看详情

kotlin协程协程异常处理③(协程异常处理器coroutineexceptionhandler捕获异常|验证coroutinescope协程的异常捕捉示例)(代码片段)

...例一、协程异常处理器CoroutineExceptionHandler捕获异常在【Kotlin协程】协程上下文(协程上下文构成要素|指定协程上下文元素组合|协程上下文元素的继承关系|协程上下文元素的几种指定形 查看详情

kotlin协程协程异常处理③(协程异常处理器coroutineexceptionhandler捕获异常|验证coroutinescope协程的异常捕捉示例)(代码片段)

...例一、协程异常处理器CoroutineExceptionHandler捕获异常在【Kotlin协程】协程上下文(协程上下文构成要素|指定协程上下文元素组合|协程上下文元素的继承关系|协程上下文元素的几种指定形 查看详情