(转)Kotlin-协程

源码 2024-9-5 11:18:06 19 0 来自 中国
上一篇:Kotlin - Lambda 表达式
协程是什么

协程并不是 Kotlin 提出来的新概念,其他的一些编程语言,比方:Go、Python 等都可以在语言层面上实现协程,甚至是 Java,也可以通过利用扩展库来间接地支持协程。
当在网上搜刮协程时,我们会看到:

  • Kotlin 官方文档说「本质上,协程是轻量级的线程」。
  • 许多博客提到「不必要从用户态切换到内核态」、「是协作式的」等等。
作为 Kotlin 协程的初学者,这些概念并不是那么容易让人明白。这些每每是作者根据自己的履历总结出来的,只看效果,而不管过程就不容易明白协程
「协程 Coroutines」源自 Simula 和 Modula-2 语言,这个术语早在 1958 年就被 Melvin Edward Conway 发明并用于构建汇编步调,阐明协程是一种编程思想,并不局限于特定的语言。
Go 语言也有协程,叫 Goroutines,从英文拼写就知道它和 Coroutines 还是有些差别的(计划思想上是有关系的),否则 Kotlin 的协程完全可以叫 Koroutines 了。
因此,对一个新术语,我们必要知道什么是「标准」术语,什么是变种。
当我们讨论协程和线程的关系时,很容易陷入中文的误区,两者都有一个「程」字,就以为有关系,实在就英文而言,Coroutines 和 Threads 就是两个概念。
从 Android 开发者的角度去明白它们的关系:

  • 我们全部的代码都是跑在线程中的,而线程是跑在进程中的。
  • 协程没有直接和操纵系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
  • 单线程中的协程总的实验时间并不会比不消协程少。
  • Android 系统上,如果在主线程举行网络哀求,会抛出 NetworkOnMainThreadException,对于在主线程上的协程也不破例,这种场景利用协程还是要切线程的。
协程计划的初志是为了办理并发题目,让 「协作式多任务」 实现起来更加方便。这里就先不睁开「协作式多任务」的概念,等我们学会了怎么用再讲。
虽说协程就是 Kotlin 提供的一套线程封装的 API,但并不是说协程就是为线程而生的。
不外,我们学习 Kotlin 中的协程,一开始确实可以从线程控制的角度来切入。由于在 Kotlin 中,协程的一个范例的利用场景就是线程控制。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不消关注多线程就可以大概很方便地写出并发操纵。
在 Java 中要实现并发操纵通常必要开启一个 Thread :
// javanew Thread(new Runnable() {    @Override    public void run() {        ...    }}).start();这里仅仅只是开启了一个新线程,至于它何时竣事、实验效果怎么样,我们在主线程中是无法直接知道的。
Kotlin 中同样可以通过线程的方式去写:
// KotlinThread({    ...}).start()可以看到,和 Java 一样也摆脱不了直接利用 Thread 的那些困难和不方便:

  • 线程什么时间实验竣事
  • 线程间的相互通讯
  • 多个线程的管理
我们可以用 Java 的 Executor 线程池来举行线程管理:
val executor = Executors.newCachedThreadPool()executor.execute({    ...})用 Android 的 AsyncTask 来办理线程间通讯:
object : AsyncTask<T0, T1, T2> {     override fun doInBackground(vararg args: T0): String { ... }    override fun onProgressUpdate(vararg args: T1) { ... }    override fun onPostExecute(t3: T3) { ... }}AsyncTask 是 Android 对线程池 Executor 的封装,但它的缺点也很显着:

  • 必要处置处罚许多回调,如果业务多则容易陷入「回调地狱」。
  • 硬是把业务拆分成了前台、中心更新、后台三个函数。
看到这里你很自然想到利用 RxJava 办理回调地狱,它确实可以很方便地办理上面的题目。
RxJava,准确来讲是 ReactiveX 在 Java 上的实现,是一种相应式步调框架,我们通过它提供的「Observable」的编程范式举行链式调用,可以很好地消除回调。
利用协程,同样可以像 Rx 那样有效地消除回调地狱,不外无论是计划理念,还是代码风格,两者是有很大区别的,协程在写法上宁静凡的次序代码类似。
这里并不会比力 RxJava 和协程哪个好,大概讨论谁代替谁的题目,我这里只给出一个发起,你最好都去相识下,由于协程和 Rx 的计划思想原来就差别。
下面的例子是利用协程举行网络哀求获取用户信息并体现到 UI 控件上:
?️launch({    val user = api.getUser() // ? 网络哀求(IO 线程)    nameTv.text = user.name  // ? 更新 UI(主线程)})这里只是展示了一个代码片断,launch 并不是一个顶层函数,它必须在一个对象中利用,我们之后再讲,这里只关心它内部业务逻辑的写法。
launch 函数加上实现在 {} 中详细的逻辑,就构成了一个协程。
通常我们做网络哀求,要不就传一个 callback,要不就是在 IO 线程里举行壅闭式的同步调用,而在这段代码中,上下两个语句分别工作在两个线程里,但写法上看起来宁静凡的单线程代码一样。
这里的 api.getUser 是一个挂起函数,以是可以大概包管 nameTv.text 的准确赋值,这就涉及到了协程中最著名的「非壅闭式挂起」。这个名词看起来不是那么容易明白,我们后续的文章会专门对这个概念举行教学。现在先把这个概念放下,只必要记住协程就是如许写的就行了。
这种「用同步的方式写异步的代码」看起来很方便吧,那么我们来看看协程详细幸亏哪。
协程幸亏哪

开始之前

在讲之前,我们必要先相识一下「闭包」这个概念,调用 Kotlin 协程中的 API,常常会用到闭包写法。
实在闭包并不是 Kotlin 中的新概念,在 Java 8 中就已经支持。
我们先以 Thread 为例,来看看什么是闭包:
?️// 创建一个 Thread 的完备写法Thread(object : Runnable {    override fun run() {        ...    }})// 满意 SAM,先简化为Thread({    ...})// 利用闭包,再简化为Thread {    ...}形如 Thread {...} 如许的结构中 {} 就是一个闭包。
在 Kotlin 中有如许一个语法糖:当函数的末了一个参数是 lambda 表达式时,可以将 lambda 写在括号外。这就是它的闭包原则。
在这里必要一个范例为 Runnable 的参数,而 Runnable 是一个接口,且只界说了一个函数 run,这种环境满意了 Kotlin 的 SAM,可以转换成传递一个 lambda 表达式(第二段),由于是末了一个参数,根据闭包原则我们就可以直接写成 Thread {...}(第三段) 的情势。
对于上文所利用的 launch 函数,可以通过闭包来举行简化 :
?️launch {    ...}根本利用

前面提到,launch 函数不是顶层函数,是不能直接用的,可以利用下面四种方法来创建协程:
?️// 方法一:利用 runBlocking 顶层函数runBlocking {    getImage(imageId)}// 方法二:利用 GlobalScope 单例对象:可以直接调用 launch 开启协程GlobalScope.launch {    getImage(imageId)}// 方法三:自行通过 CoroutineContext 创建一个 CoroutineScope 对象// 必要一个范例为 CoroutineContext 的参数val coroutineScope = CoroutineScope(context)coroutineScope.launch {    getImage(imageId)}//方法四:async 函数启动新的协程,通过 await 获取效果coroutineScope.launch(Dispatchers.Main) {    // async 函数启动新的协程    val avatar: Deferred = async { api.getAvatar(user) }    // 获取用户头像    val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo    // 获取返回值    show(avatar.await(), logo.await())                     // 更新 UI}

  • 方法一通常实用于单元测试的场景,而业务开发中不会用到这种方法,由于它是线程壅闭的。
  • 方法二和利用 runBlocking 的区别在于不会壅闭线程。但在 Android 开发中同样不保举这种用法,由于它的生命周期会和 app 划一,且不能取消(什么是协程的取消背面的文章会讲)。
  • 方法三是比力保举的利用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合利用)。
    关于 CoroutineScope 和 CoroutineContext 的更多内容背面的文章再讲。
接下来我们紧张来对比 launch 与 async 这两个函数。

  • 雷同点:它们都可以用来启动一个协程,返回的都是 Coroutine,我们这里不必要纠结详细是返回哪个类。
  • 差别点:async 返回的 Coroutine 多实现了 Deferred 接口。
可以看到 方法四中 avatar 和 logo 的范例可以声明为 Deferred ,通过 await 获取效果而且更新到 UI 上体现。
await 函数署名如下:
public suspend fun await(): T关于 Deferred 更深入的知识就不在这里过多论述,它的意思就是耽误,也就是效果稍后才能拿到。
我们调用 Deferred.await() 就可以得到效果了。
协程最常用的功能是并发,而并发的范例场景就是多线程。可以利用 Dispatchers.IO 参数把任务切到 IO 线程实验:
coroutineScope.launch(Dispatchers.IO) {    ...}也可以利用 Dispatchers.Main 参数切换到主线程:
?️coroutineScope.launch(Dispatchers.Main) {    ...}以是在「协程是什么」一节中讲到的异步哀求的例子完备写出来是如许的:
?️coroutineScope.launch(Dispatchers.Main) {   // 在主线程开启协程    val user = api.getUser() // IO 线程实验网络哀求    nameTv.text = user.name  // 主线程更新 UI}而通过 Java 实现以上逻辑,我们通常必要如许写:
☕️api.getUser(new Callback<User>() {    @Override    public void success(User user) {        runOnUiThread(new Runnable() {            @Override            public void run() {                nameTv.setText(user.name);            }        })    }    @Override    public void failure(Exception e) {        ...    }});这种回调式的写法,打破了代码的次序结构和完备性,读起来相称难熬。
协程的「1 到 0」

对于回调式的写法,如果并发场景再复杂一些,代码的嵌套大概会更多,如许的话维护起来就非常贫苦。但如果你利用了 Kotlin 协程,多层网络哀求只必要这么写:
?️coroutineScope.launch(Dispatchers.Main) {       // 开始协程:主线程    val token = api.getToken()                  // 网络哀求:IO 线程    val user = api.getUser(token)               // 网络哀求:IO 线程    nameTv.text = user.name                     // 更新 UI:主线程}如果碰到的场景是多个网络哀求必要等待全部哀求竣事之后再对 UI 举行更新。好比以下两个哀求:
?️api.getAvatar(user, callback)api.getCompanyLogo(user, callback)如果利用回调式的写法,那么代码大概写起来既困难又别扭。于是我们大概会选择妥协,通过先后哀求代替同时哀求:
?️api.getAvatar(user) { avatar ->    api.getCompanyLogo(user) { logo ->        show(merge(avatar, logo))    }}在现实开发中如果如许写,原来可以大概并行处置处罚的哀求被欺压通过串行的方式去实现,大概会导致等待时间长了一倍,也就是性能差了一倍。
而如果利用协程,可以直接把两个并行哀求写成上下两行,末了再把效果举行归并,即接纳上述的方法四:
?️coroutineScope.launch(Dispatchers.Main) {    //            ?  async 函数之后再讲    val avatar = async { api.getAvatar(user) }    // 获取用户头像    val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo    val merged = suspendingMerge(avatar, logo)    // 归并效果    //                  ?    show(merged) // 更新 UI}可以看到,即便是比力复杂的并行网络哀求,也可以大概通过协程写出结构清晰的代码。必要注意的是 suspendingMerge 并不是协程 API 中提供的方法,而是我们自界说的一个可「挂起」的效果归并方法。
让复杂的并发代码,写起来变得简朴且清晰,是协程的上风。
这里,两个没有相干性的后台任务,由于用了协程,被安排得显着确白,相互之间配合得很好,也就是我们之前说的「协作式任务」。
原来必要回调,现在直接没有回调了,这种从 1 到 0 的计划思想真的妙哉。
在相识了协程的作用和上风之后,我们再来看看协程是怎么利用的。
协程怎么用

在项目中设置对 Kotlin 协程的支持

在利用协程之前,我们必要在 build.gradle 文件中增长对 Kotlin 协程的依赖:

  • 项目根目次下的 build.gradle :
//groovybuildscript {    ...    ext.kotlin_coroutines = '1.3.1'    ...}

  • Module 下的 build.gradle :
//Groovydependencies {    ...    //                                       ? 依赖协程核心库    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-corekotlin_coroutines"    //                                       ? 依赖当前平台所对应的平台库    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-androidkotlin_coroutines"    ...}Kotlin 协程是以官方扩展库的情势举行支持的。而且,我们所利用的「核心库」和 「平台库」的版本应该保持划一。

  • 核心库中包罗的代码紧张是协程的公共 API 部分。有了这一层公共代码,才使得协程在各个平台上的接口得到同一。
  • 平台库中包罗的代码紧张是协程框架在详细平台的详细实现方式。由于多线程在各个平台的实现方式是有所差别的。
完成了以上的准备工作就可以开始利用协程了。
开始利用协程

协程最简朴的利用方法,其着实前面章节就已经看到了。我们可以通过一个 launch 函数实现线程切换的功能:
?️coroutineScope.launch(Dispatchers.IO) {    ...}这个 launch 函数,它详细的寄义是:我要创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓「协程」是谁?就是你传给 launch 的那些代码,这一段一连代码叫做一个「协程」。
以是,什么时间用协程?当你必要切线程大概指定线程的时间。你要在后台实验任务?切!
?️launch(Dispatchers.IO) {    val image = getImage(imageId)}然后必要在前台更新界面?再切!
?️coroutineScope.launch(Dispatchers.IO) {    val image = getImage(imageId)    launch(Dispatchers.Main) {        avatarIv.setImageBitmap(image)    }}好像有点不对劲?这不还是有嵌套嘛。
如果只是利用 launch 函数,协程并不能比线程做更多的事。不外协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑实验竣事之后,自动把线程切归去继承实验。那么可以将上面的代码写成如许:
?️coroutineScope.launch(Dispatchers.Main) {      // ? 在 UI 线程开始    val image = withContext(Dispatchers.IO) {  // ? 切换到 IO 线程,并在实验完成后切回 UI 线程        getImage(imageId)                      // ? 将会运行在 IO 线程    }    avatarIv.setImageBitmap(image)             // ? 回到 UI 线程更新 UI} 这种写法看上去好像和刚才那种区别不大,但如果你必要频仍地举行线程切换,这种写法的上风就会体现出来。可以参考下面的对比:
?️// 第一种写法coroutineScope.launch(Dispatchers.IO) {    ...    launch(Dispatchers.Main){        ...        launch(Dispatchers.IO) {            ...            launch(Dispatchers.Main) {                ...            }        }    }}// 通过第二种写法来实现雷同的逻辑coroutineScope.launch(Dispatchers.Main) {    ...    withContext(Dispatchers.IO) {        ...    }    ...    withContext(Dispatchers.IO) {        ...    }    ...}由于可以"自动切返来",消除了并发代码在协作时的嵌套。由于消除了嵌套关系,我们甚至可以把 withContext 放进一个单独的函数内里:
?️launch(Dispatchers.Main) {              // ? 在 UI 线程开始    val image = getImage(imageId)    avatarIv.setImageBitmap(image)     // ? 实验竣事后,自动切换回 UI 线程}//                               ?fun getImage(imageId: Int) = withContext(Dispatchers.IO) {    ...}这就是之前说的「用同步的方式写异步的代码」了。
不外如果只是如许写,编译器是会报错的:
?️fun getImage(imageId: Int) = withContext(Dispatchers.IO) {    // IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion}意思是说,withContext 是一个 suspend 函数,它必要在协程大概是另一个 suspend 函数中调用。
suspend

suspend 是 Kotlin 协程最核心的关键字,险些全部先容 Kotlin 协程的文章和演讲都会提到它。它的中文意思是「停息」大概「可挂起」。如果你去看一些技能博客或官方文档的时间,大概可以相识到:当「代码实验到 suspend 函数的时间所在的协程就会『挂起』,而且这个『挂起』好坏壅闭式的,它不会壅闭你当前的线程。」
上面报错的代码,实在只必要在前面加一个 suspend 就可以大概编译通过:
?️suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {    ...}以是接下来,我们的核心内容就是来好好说一说这个「挂起」。
「挂起」的本质

什么是挂起?挂起,就是一个稍后会被自动切返来的线程调治操纵。
协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?都不对,我们挂起的对象是协程。
还记得协程是什么吗?启动一个协程可以利用 launch 大概 async 函数,协程实在就是这两个函数中闭包的代码块。
launch ,async 大概其他函数创建的协程,在实验到某一个 suspend 函数的时间,这个协程会被「suspend」,也就是被挂起。
那此时又是从那边挂起?从当火线程挂起。换句话说,就是这个协程从正在实验它的线程上脱离。
注意,不是这个协程停下来了!是脱离,当火线程不再管这个协程要去做什么了。
suspend 是有停息的意思,但我们在协程中应该明白为:当线程实验到协程的 suspend 函数的时间,暂时不继承实验协程代码了。
我们先让时间静止,然后兵分两路,分别看看这两个相互脱离的线程和协程接下来将会发生什么事变:
线程:
前面我们提到,挂起会让协程从正在实验它的线程上脱离,详细到代码实在是:
协程的代码块中,线程实验到了 suspend 函数这里的时间,就暂时不再实验剩余的协程代码,跳出协程的代码块。
那线程接下来会做什么呢?
如果它是一个后台线程:

  • 要么无事可做,被系统接纳
  • 要么继承实验别的后台任务
跟 Java 线程池里的线程在工作竣事之后是完全一样的:接纳大概再利用。
如果这个线程它是 Android 的主线程,那它接下来就会继承归去工作:也就是一秒钟 60 次的界面革新任务。
一个常见的场景是,获取一个图片,然后体现出来:
?️// 主线程中GlobalScope.launch(Dispatchers.Main) {  val image = suspendingGetImage(imageId)  // 获取图片  avatarIv.setImageBitmap(image)           // 体现出来}suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {  ...}这段实验在主线程的协程,它实质上会往你的主线程 post 一个 Runnable,这个 Runnable 就是你的协程代码:
?️handler.post {  val image = suspendingGetImage(imageId)  avatarIv.setImageBitmap(image)}当这个协程被挂起的时间,就是主线程 post 的这个 Runnable 提前竣事,然后继承实验它界面革新的任务。
关于线程,我们就看完了。
这个时间你大概会有一个疑问,那 launch 包裹的剩下代码怎么办?
以是接下来,我们来看看协程这一边。
协程:
线程的代码在到达 suspend 函数的时间被掐断,接下来协程会从这个 suspend 函数开始继承往下实验,不外是在指定的线程
谁指定的?是 suspend 函数指定的,好比我们这个例子中,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程。
Dispatchers 调治器,它可以将协程限定在一个特定的线程实验,大概将它分派到一个线程池,大概让它不受限定地运行,关于 Dispatchers 这里先不睁开了。
那我们通常里常用到的调治器有哪些?
常用的 Dispatchers ,有以下三种:

  • Dispatchers.Main:Android 中的主线程
  • Dispatchers.IO:针对磁盘和网络 IO 举行了优化,适合 IO 麋集型的任务,好比:读写文件,操纵数据库以及网络哀求
  • Dispatchers.Default:适合 CPU 麋集型的任务,好比盘算
回到我们的协程,它从 suspend 函数开始脱离启动它的线程,继承实验在 Dispatchers 所指定的 IO 线程。
紧接着在 suspend 函数实验完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切返来
这个「切返来」是什么意思?
我们的协程原本是运行在主线程的,当代码碰到 suspend 函数的时间,发生线程切换,根据 Dispatchers 切换到了 IO 线程;
当这个函数实验完毕后,线程又切了返来,「切返来」也就是协程会帮我再 post 一个 Runnable,让我剩下的代码继承回到主线程去实验。
我们从线程和协程的两个角度都分析完成后,终于可以对协程的「挂起」suspend 做一个表明:
协程在实验到有 suspend 标记的函数的时间,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;
不外区别在于,挂起函数在实验完成之后,协程会重新切回它原先的线程
再简朴来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切返来的线程调治操纵
这个「切返来」的动作,在 Kotlin 里叫做 resume,规复。
通过刚才的分析我们知道:挂起之后是必要规复。
而规复这个功能是协程的,如果你不在协程内里调用,规复这个功能没法实现,以是也就答复了这个题目:为什么挂起函数必须在协程大概另一个挂起函数里被调用。
再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它实在直接大概间接地,总是会在一个协程里被调用的。
以是,要求 suspend 函数只能在协程里大概另一个 suspend 函数里被调用,还是为了要让协程可以大概在 suspend 函数切换线程之后再切返来。
怎么就「挂起」了?

我们相识到了什么是「挂起」后,再接着看看这个「挂起」是怎么做到的。
先恣意写一个自界说的 suspend 函数:
?️suspend fun suspendingPrint() {  println("Thread: ${Thread.currentThread().name}")}I/System.out: Thread: main输出的效果还是在主线程。
为什么没切换线程?由于它不知道往哪切,必要我们告诉它。
对比之前例子中 suspendingGetImage 函数代码:
?️suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {  ...}我们可以发现差别之处其着实于 withContext 函数。
实在通过 withContext 源码可以知道,它自己就是一个挂起函数,它吸收一个 Dispatcher 参数,依赖这个 Dispatcher 参数的指示,你的协程被挂起,然后切到别的线程。
以是这个 suspend,实在并不是起到把任何把协程挂起,大概说切换线程的作用。
真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的。
以是我们想要自己写一个挂起函数,仅仅只加上 suspend 关键字是不可的,还必要函数内部直接或间接地调用到 Kotlin 协程框架自带的 suspend 函数才行。
suspend 的意义?

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?
它实在是一个提示。
函数的创建者对函数的利用者的提示:我是一个耗时函数,我被我的创建者用挂起的方式放在后台运行,以是请在协程里调用我。
为什么 suspend 关键字并没有现实去操纵挂起,但 Kotlin 却把它提供出来?
由于它原来就不是用来操纵挂起的。
挂起的操纵 —— 也就是切线程,依赖的是挂起函数内里的现实代码,而不是这个关键字。
以是这个关键字,只是一个提示
还记得刚才我们实验自界说挂起函数的方法吗?
?️// ? redundant suspend modifiersuspend fun suspendingPrint() {  println("Thread: ${Thread.currentThread().name}")}如果你创建一个 suspend 函数但它内部不包罗真正的挂起逻辑,编译器会给你一个提示:redundant suspend modifier,告诉你这个 suspend 是多余的。
由于你这个函数实质上并没有发生挂起,那你这个 suspend 关键字只有一个效果:就是限定这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。
以是,创建一个 suspend 函数,为了让它包罗真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数,你的这个 suspend 才是故意义的。
怎么自界说 suspend 函数?

在相识了 suspend 关键字的来龙去脉之后,我们就可以进入下一个话题了:怎么自界说 suspend 函数。
这个「怎么自界说」实在分为两个题目:

  • 什么时间必要自界说 suspend 函数?
  • 详细该怎么写呢?
什么时间必要自界说 suspend 函数

如果你的某个函数比力耗时,也就是要等的操纵,那就把它写成 suspend 函数。这就是原则。
耗时操纵一样寻常分为两类:I/O 操纵和 CPU 盘算工作。好比文件的读写、网络交互、图片的暗昧处置处罚,都是耗时的,通通可以把它们写进 suspend 函数里。
别的这个「耗时」尚有一种特别环境,就是这件事自己做起来并不慢,但它必要等待,好比 5 秒钟之后再做这个操纵。这种也是 suspend 函数的应用场景。
详细该怎么写

给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。
提到用 withContext是由于它在挂起函数里功能最简朴直接:把线程自动切走和切回。
固然并不是只有 withContext 这一个函数来辅助我们实现自界说的 suspend 函数,好比尚有一个挂起函数叫 delay,它的作用是等待一段时间后再继承往下实验代码。
利用它就可以实现刚才提到的等待范例的耗时操纵:
?️suspend fun suspendUntilDone() {  while (!done) {    delay(5)  }}这些东西,在我们开端利用协程的时间不消立马接触,可以先把协程最根本的方法和概念理清晰。
好,关于协程中的「挂起」我们就表明到这里。
大概你心中还会存在一些迷惑:

  • 协程中挂起的「非壅闭式」到底是怎么回事?
  • 协程和 RxJava 在切换线程方面功能是一样的,都能让你写出制止嵌套回调的复杂并发代码,那协程尚有哪些上风,大概让开发者利用协程的理由?
非壅闭式挂起

什么是「非壅闭式挂起」
非壅闭式是相对壅闭式而言的。
编程语言中的许多概念实在都泉源于生存,就像脱口秀的段子一样。
线程壅闭很好明白,现实中的例子就是交通堵塞,它的核心有 3 点:
前面有停滞物,你过不去(线程卡了)
必要等停滞物扫除后才能已往(耗时任务竣事)
除非你绕道而行(切到别的线程)
从语义上明白「非壅闭式挂起」,讲的是「非壅闭式」这个是挂起的一个特点,也就是说,协程的挂起,就好坏壅闭式的,协程是不讲「壅闭式的挂起」的概念的。
我们讲「非壅闭式挂起」,实在它有几个条件:并没有限定在一个线程里说这件事,由于挂起这件事,原来就是涉及到多线程。
就像视频里讲的,壅闭不壅闭,都是针对单线程讲的,一旦切了线程,肯定好坏壅闭的,你都跑到别的线程了,之前的线程就自由了,可以继承做别的事变了。
以是「非壅闭式挂起」,实在就是在讲协程在挂起的同时切线程这件事变。
为什么要讲非壅闭式挂起
由于它在写法上和单线程的壅闭式是一样的。
协程只是在写法上「看起来壅闭」,实在是「非壅闭」的,由于在协程内里它做了许多工作,此中有一个就是帮我们切线程。
挂起,重点是说切线程先切已往,然后再切返来。
非壅闭式,重点是说线程固然会切,但写法上宁静凡的单线程差不多。
让我们来看看下面的例子:
?️main {    GlobalScope.launch(Dispatchers.Main) {        // ? 耗时操纵        val user = suspendingRequestUser()        updateView(user)    }        private suspend fun suspendingRequestUser() : User = withContext(Dispatchers.IO) {        api.requestUser()    }}从上面的例子可以看到,耗时操纵和更新 UI 的逻辑像写单线程一样放在了一起,只是在外貌包了一层协程。
而正是这个协程办理了原来我们单线程写法会卡线程这件事。
壅闭的本质
起首,全部的代码本质上都是壅闭式的,而只有比力耗时的代码才会导致人类可感知的等待,好比在主线程上做一个耗时 50 ms 的操纵会导致界面卡掉几帧,这种是我们人眼能观察出来的,而这就是我们通常意义所说的「壅闭」。
举个例子,当你开发的 app 在性能好的手机上很流通,在性能差的老手机上会卡顿,就是在说同一行代码实验的时间不一样。
视频中讲了一个网络 IO 的例子,IO 壅闭更多是反映在「等」这件事变上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。
而这跟协程半毛钱关系没有,切线程办理不了的事变,协程也办理不了。
协程与线程
Kotlin 协程和线程是无法脱离开讲的。
别的语言我不说,在 Kotlin 里,协程就是基于线程来实现的一种更上层的工具 API,类似于 Java 自带的 Executor 系列 API 大概 Android 的 Handler 系列 API。
只不外呢,协程它不仅提供了方便的 API,在计划思想上是一个基于线程的上层框架,你可以明白为新造了一些概念用来资助你更好地利用这些 API,仅此而已。
就像 ReactiveX 一样,为了让你更好地利用各种操纵符 API,新造了 Observable 等概念。
说到这里,Kotlin 协程的三大疑问:协程是什么、挂起是什么、挂起的非壅闭式是怎么回事,就已经全部讲完了。非常简朴:
协程就是切线程;
挂起就是可以自动切返来的切线程;
挂起的非壅闭式指的是它能用看起来壅闭的代码写出非壅闭的操纵,就这么简朴。
还改正了官方文档内里的一个错误,这里就不再重复了,末了想表达一点:
Kotlin 协程并没有脱离 Kotlin 大概 JVM 创造新的东西,它只是将多线程的开发变得更简朴了,可以说是由于 Kotlin 的诞生而灵活壮丽出现的东西,从语法上看它很神奇,但从原理上讲,它并不是把戏。
上一篇:Kotlin - Lambda 表达式
转自:https://rengwuxian.com/kotlin-coroutines-1/
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 18:28, Processed in 0.183051 second(s), 32 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表