Android Kotlin 协程详解

前言

关于Kotlin基础和高阶函数又不熟悉的可以先参考文章:

Android Kotlin 基础详解_袁震的博客-CSDN博客

Android Kotlin 高阶详解_袁震的博客-CSDN博客

什么是协程?要理解协程,就要将它和线程联系起来理解。

线程是什么?我想大家都清楚,而协程,它比线程更加轻量级,一个线程上面可以有多个协程。

如果我们应用开启一万个线程,可能就崩溃了,但是如果我们开启10万个协程,对应用的性能也不会有太大的影响。

协程是可挂起计算的实例,它需要一个代码块运行,并具有类似的生命周期(可以被创建、启动和取消),它不绑定到任何特定的线程,可以在一个线程中挂起其执行,并在另一个线程中恢复,它在完结时可能伴随着某种结果(值或异常)

协程的主要作用:

1,处理耗时任务,这种任务可能会阻塞主线程

2.,保证线程安全

一,协程的基本使用

Kotlin并没有将协程纳入标准库的API当中,而是以依赖库的形式提供的。所以如果我们想要使用协程功能,需要先在app/build.gradle文件当中添加如下依赖库:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
fun main() {GlobalScope.launch {//开启一个协程println("开启一个协程")}
}

注意:这段代码是没有输出的。后面会解释原因。 

二,协程的作用域 CoroutineScope

协程的作用域主要包括三种:

顶级作用域没有父协程的协程所在的作用域为顶级作用域。
协同作用域 协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
主从作用域 与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。

除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
子协程会继承父协程的协程上下文中的元素,如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。

 

在kotlin中,所有的作用域都是CoroutineScope 的子类,下面是四种常用的作用域:

GlobalScope全局范围,不会自动结束执行。
MainScope主线程的作用域,全局范围
lifecycleScope生命周期范围,用于activity等有生命周期的组件,在Desroyed的时候会自动结束。
viewModeScopeViewModel范围,用于ViewModel中,在ViewModel被回收时会自动结束

①GlobalScope:

 GlobalScope是全局的作用域,并且是无法取消的,因为:

public object GlobalScope : CoroutineScope {override val coroutineContext: CoroutineContextget() = EmptyCoroutineContext
}

GlibalScope的源码可以看出,他的上下文对象是EmptyCorountineContext ,并没有Job对象,所以我们无法通过Job对象去cancle协程。

fun main() {GlobalScope.launch {//开启一个协程println("开启一个协程")}
}

这段代码为什么没有输出呢? 因为GlibalScope 是不阻塞线程的,主线程执行完了,此协程也会跟着结束,所以没有输出。

fun main() {GlobalScope.launch {//开启一个协程println("开启一个协程")}Thread.sleep(1000)
}

那如果我这样写,就会有输出打印 

②MainScope:

MainScope也是全局的作用域,但是它是可以取消的。

public fun MainScope(): CoroutineScope= ContextScope(SupervisorJob() + Dispatchers.Main)

它的上下文是 SupervisorJob()和主线程调度器构成的,所以它是可以取消的全局主线程协程。

MainScope是一个全局函数,我们可以在任何地方调用它(Activity,Fragment,Dialog,ViewModel等),但是需要注意在页面销毁的时候,需要手动cancle。

class MainActivity : AppCompatActivity() {var mScope=MainScope()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)mScope.launch { println("执行了协程")}}override fun onDestroy() {super.onDestroy()mScope.cancel()}
}

③ViewModelScope

ViewModelScope 是一个CloseableCoroutineScope,它的上下文由 SupervisorJob() + Dispatchers.Main.immediate构成,所以它也是可以取消的主线程协程

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"val ViewModel.viewModelScope: CoroutineScopeget() {val scope: CoroutineScope? = this.getTag(JOB_KEY)if (scope != null) {return scope}return setTagIfAbsent(JOB_KEY, CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))}internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {override val coroutineContext: CoroutineContext = contextoverride fun close() {coroutineContext.cancel()}
}

可以看到 ViewModelScope是 ViewModel类的 扩展属性,假如这个  ViewModel 是 Activity 的,那么在 Activity 退出的时候,ViewModel 的  clear() 方法就会被调用,而  clear() 方法中会扫描当前 ViewModel 的成员  mBagsOfTags(一个Map对象)中保存的所有的  Closeable 的  object 对象(也就是上面的 CloseableCoroutineScope),并调用其  close() 方法。
所以当它用在ViewModel里面的时候,我们不用主动去回收它,它会自动回收。

④LifecycleScope:

LifecycleScope的实例是LifecycleCoroutineScopeImpl,它的上下文也是由SupervisorJob()+Dispatchers.Main.immediate构成所以它也是可以取消的主线程协程

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScopeget() = lifecycle.coroutineScopepublic val Lifecycle.coroutineScope: LifecycleCoroutineScopeget() {while (true) {val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?if (existing != null) {return existing}val newScope = LifecycleCoroutineScopeImpl(this,SupervisorJob() + Dispatchers.Main.immediate)if (mInternalScopeRef.compareAndSet(null, newScope)) {newScope.register()return newScope}}}

lifecycleScope是LifecycleOwner的扩展属性,因此它只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期。 它的基本使用和 viewModelScope 是一样的。但是它多了生命周期的一些感知。它也是通过  LifecycleController 中为 Lifecycle注册 观察者接口, 来感知 onResume的状态,然后进行调用的。

public fun launchWhenCreated(block: suspend CoroutineScope.() -> Unit): Job = launch {lifecycle.whenCreated(block)
}
public fun launchWhenStarted(block: suspend CoroutineScope.() -> Unit): Job = launch {lifecycle.whenStarted(block)
}
public fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch { lifecycle.whenResumed(block)
}

三,启动协程的方式

协程的启动,主要有五种方式:

调用  xxxScope.launch{...}  启动一个协程块, launch方法启动的协程不会将结果返回给调用方。

GlobalScope.launch {//开启一个协程println("开启一个GlobalScope协程")
}


在  xxxScope {...} 中调用  async{...} 创建一个子协程, async会返回一个 Deferred对象,随后可以调用 Deferred对象的 await()方法来启动该协程。

fun main() {GlobalScope.launch {//开启一个协程println("开启一个GlobalScope协程")val result =async {println("async")10}println("result:${result.await()}")}Thread.sleep(1000)
}

这里需要注意一下:async函数必须在协程作用域当中才能调用,await()是一个挂起函数,只能在挂起作用域内调用。所以通常不用async{}来创建最外层的协程,因为非挂起作用域无法调用await()函数获取协程的返回值。所以返回值没有意义,这样的话async()的返回值Deferred就是普通的Job,所以完全可以使用launch{}代替async{}


withContext(){...} 一个 suspend方法,在给定的上下文中执行并返回结果,它的目的不在于启动子协程,主要用于 线程切换,将长耗时操作从UI线程切走,完事再切回来。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。 

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)mScope.launch {println("执行了协程")val result =withContext(Dispatchers.IO){println("切换到子线程")20}println(result)withContext(Dispatchers.Main){println("切换到主线程")}}
}

withContext()是顶级函数,可以直接调用(不需要创建协程对象)。但是withContext()是一个suspend挂起函数,它只能在协程或其他挂起函数中调用(必须先有协程)

调用withContext()函数之后,会立即执行代码块中的代码,同时将外部协程挂起。当代码块中的代码全部执行完之后,会将最后一行的执行结果作为withContext()函数的返回值返回

withContext()函数强制要求我们指定一个线程参数,这个参数就是调度器,下面会讲


coroutineScope{...} &supervisorScope{...},创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解。

private fun request() {lifecycleScope.launch {coroutineScope { // 协同作用域,抛出未捕获异常时会取消父协程launch { }}supervisorScope { // 主从作用域,抛出未捕获异常时不会取消父协程launch { }}}
}

coroutineScope  表示 协同作用域,  内部的协程 出现异常 会向外部传播,子协程未捕获的异常会向上传递给父协程,  子协程 可以挂掉外部协程 , 外部协程挂掉也会挂掉子协程,即 双向传播 。 任何一个子协程异常退出,会导致整体的退出。
supervisorScope 表示 主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,内部的 子协程挂掉 不会影响外部的父协程和兄弟协程的继续运行,它就像一道防火墙,隔离了异常,保证程序健壮,但是如果外部协程挂掉还是可以取消子协程的,即 单向传播。它的设计应用场景多用于 子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。
 

⑤runBlocking{...} 创建一个协程,并阻塞当前线程,直到协程执行完毕。

fun main() {runBlocking {println("开启一个runBlocking协程")}
}

 一般的开发中我们尽量不使用这种方式,它通常用于main函数或者其他测试用例中,因为在main函数中启动一个协程去执行耗时任务,如果不阻塞main函数的线程,main函数执行完jvm就退出了,为了避免jvm退出,通常在最后需要Thread.sleep(Long.MAX_VALUE)让主线程休眠来等待协程执行完毕。但是如果使用runBlocking{}创建协程就不会出现jvm提前退出的问题。如在前面提到的不打印问题

四, 协程启动模式

在创建协程时,一般是有四种启动模式,如果我们不写的话,一般是默认的DEFAULT模式

①CoroutineStart.DEFAULT: 协程创建后,立即开始调度,但 有可能在执行前被取消。在调度前如果协程被取消,其将直接进入取消响应的状态。

mScope.launch (start =CoroutineStart.DEFAULT){println("执行了协程")val result =withContext(Dispatchers.IO){println("切换到子线程")20}println(result)withContext(Dispatchers.Main){println("切换到主线程")}
}

②CoroutineStart.ATOMIC  协程创建后,立即开始调度, 协程执行到第一个挂起点之前不响应取消。其将调度和执行两个步骤合二为一,就像它的名字一样,其保证调度和执行是原子操作,因此协程也 一定会执行

mScope.launch (start =CoroutineStart.ATOMIC){println("执行了协程")val result =withContext(Dispatchers.IO){println("切换到子线程")20}println(result)withContext(Dispatchers.Main){println("切换到主线程")}
}

③CoroutineStart.LAZY  只要协程被需要时(主动调用该协程的 start、 join、 await等函数时 , 才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。

mScope.launch (start =CoroutineStart.LAZY){println("执行了协程")val result =withContext(Dispatchers.IO){println("切换到子线程")20}println(result)withContext(Dispatchers.Main){println("切换到主线程")}
}

④CoroutineStart.UNDISPATCHED  协程创建后,立即在当前线程中执行,直到遇到第一个真正挂起的点。是立即执行,因此协程 一定会执行

mScope.launch (start =CoroutineStart.UNDISPATCHED){println("执行了协程")val result =withContext(Dispatchers.IO){println("切换到子线程")20}println(result)withContext(Dispatchers.Main){println("切换到主线程")}

五,协程调度器

官方框架中预置了 4 个调度器,我们可以通过  Dispatchers 对象访问它们

①Default: 默认调度器 ,适合处理后台计算,其是一个  CPU 密集型任务调度器

②IO: IO 调度器,适合执行 IO 相关操作,其是  IO 密集型任务调度器

③Main: UI 调度器,根据平台不同会被初始化为对应的 UI 线程的调度器, 例如在Android 平台上它会将协程调度到 UI 事件循环中执行,即通常在 主线程上执行。

④Unconfined:“无所谓“调度器,不要求协程执行在特定线程上。协程的调度器如果是 Unconfined, 那么它在挂起点恢复执行时会在恢复所在的线程上直接执行,当然, 如果嵌套创建以它为调度器的协程,那么这些协程会在启动时被调度到协程框架内部的事件循环上,以避免出StackOverflow。
 

如果创建 Coroutine的时候未指定调度器,或者使用未指定的调度器的上下文的 Scope通过 launch或 async启动一个协程,则默认是使用 Dispatchers.Default调度器 

由于 子协程会默认继承 父协程的 context上下文,所以一般我们可以直接为 父协程的 context上设置一个 Dispatcher,这样所有的子协程就自动使用这个 Dispatcher,当某个子协程有特殊需要的时候再其指定特定的 Dispatcher。
 

Default 和  IO 这两个调度器背后实际上是 同一个线程池。为什么二者在使用上会存在差异呢?由于 IO 任务通常会阻塞实际执行任务的线程,在阻塞过程中线程虽然不占用 CPU,  但却占用了大量内存,这段时间内被 IO 任务占据线程实际上是资源使用不合理的表现,因此 IO 调度器对于 IO 任务的并发做了限制, 避免过多的 IO 任务并发占用过多的系统资源,同时在调度时为任务打上 PROBABLY BLOCKING 标签,以方便线程池在执行任务调度时对阻塞任务和非阻塞任务区别对待。
 

六,suspend

在Kotlin协程中,被suspend修饰的函数是一个挂起函数,可以调用和使用协程库里的方法,仅能被suspend修饰的方法或lambda闭包使用,在其余地方使用会编译报错,因为被suspend修饰的函数再编译成java后会增加一个Continuation的参数,这个函数用于回调协程执行的结果,所以说协程的异步调用本质上就是一次异步调用。

当在协程中调用到挂起函数时,协程就会在当前线程(主线程)中被挂起,这就是协程中著名的非阻塞式挂起,主线程暂时停止执行这个协程中剩余的代码,注意:暂停并不是阻塞等待(否则会ANR),而是主线程暂时从这个协程中被释放出来去处理其他Handler消息,比如响应用户操作、绘制View等等。那挂起函数谁执行?这得看挂起函数内部是否有切换线程,如果没有切换线程当然就是主线程(当前线程)执行了,所以挂起函数不一定就是在子线程中执行的,但是通常在定义挂起函数时都会为它指定其他线程,这样挂起才有意义。

fun main(args: Array<String>) {runBlocking {println(Thread.currentThread().name)var html = getHtml()println(Thread.currentThread().name)}
}suspend fun getHtml(): String {return GlobalScope.async {println(Thread.currentThread().name)delay(1000)URL("https://www.baidu.com").readText()}.await()
}

比如上面的例子,在主线程(默认线程)中启动了一个协程,当执行到getHtml()时,切换到了GlobalScope协程中(默认运行在工作线程中)去执行,此时主线程中的代码将被暂时挂起。当getHtml中的方法执行完并返回后,主线程中的协程才会继续运行,这叫做协程恢复,如果遇到了其他挂起函数还会重复这个过程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/83626.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

个人所思所想录

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;CSDN实力新星&#xff0c;后端开发两年经验&#xff0c;曾担任甲方技术代表&#xff0c;业余独自创办智源恩创网络科技工作室。会点点Java相关技术栈、帆软报表、低代码平台快速开…

Java版分布式微服务云开发架构 Spring Cloud+Spring Boot+Mybatis 电子招标采购系统功能清单

项目说明 随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大&#xff0c;公司对内部招采管理的提升提出了更高的要求。在企业里建立一个公平、公开、公正的采购环境&#xff0c;最大限度控制采购成本至关重要。符合国家电子招投标法律法规及相关规范&#xff0c;以及审…

在VMware虚拟机中固定CentOS系统ip(使用桥接模式)

目录 一、前置说明二、前置准备2.1、切换虚拟机网络为桥接模式2.2、查看本机网络信息 三、配置CentOS系统IP3.1、进入系统输入ip addr 查看本机网络配置名称3.2、查看网络配置目录&#xff0c;网络配置文件名称3.3、修改网络配置文件 ifcfg-ens33 固定IP3.4、重启网络 一、前置…

mac+idea中安装配置gradle方法

1. mac中安装gradle 1) 安装brew 2) 通过brew安装gradle brew install gradle 3) 配置JAVA_HOME环境变量 export JAVA_HOME$(/usr/libexec/java_home) 至此&#xff0c;gradle安装成功 2. idea中配置gradle 配置位置&#xff1a; idea->Settings->Build,Executi…

element table合并行或列 span-method

首先来看下官网上如何写的 <template><div><el-table:data"tableData":span-method"objectSpanMethod"borderstyle"width: 100%; margin-top: 20px"><el-table-columnprop"id"label"ID"width"18…

linux下解决tomcat错误问题

错误一&#xff1a; Linux下Tomcat启动报错&#xff1a;Neither the JAVA_HOME nor the JRE_HOME environment variable is defined 原因&#xff1a;可能是Linux环境变了&#xff0c;需要在catalina.sh文件里指定JDK路径 解决方式&#xff1a; 在/bin/catalina.sh配置文件中加…

Vue语法

目录 事件处理器 是什么 案列 表单的综合案列 定义 常用功能 组件通信 定义 父传子 ​编辑 子传父 事件处理器 是什么 事件处理器是一种函数&#xff0c;用于响应和处理事件的触发。在编程中&#xff0c;当特定事件发生时&#xff0c;可以通过事件处理器来执行相应的…

八大排序(一)冒泡排序,选择排序,插入排序,希尔排序

一、冒泡排序 冒泡排序的原理是&#xff1a;从左到右&#xff0c;相邻元素进行比较。每次比较一轮&#xff0c;就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。 以从小到大排序为例&#xff0c;第一轮比较后&#xff0c;所有数中最大的那个数就会浮…

go语言初学(备忘)

1、安装 2 路径配置 C:\Program Files\Go\bin 3新建一个工程 4、下载VSCode 并安装插件 创建一个调试文件 在main目录下新建一个test.go脚本 package main import "fmt" func main() { fmt.Println("Hi 1111") fmt.Println("testasdf") } 断点…

MySQL学习系列(5)-每天学习10个知识

目录 1. 锁&#xff08;Locking&#xff09;和乐观锁与悲观锁2. 分布式系统中保证数据一致性3. MySQL的复制延迟问题及解决方法4. 索引比全表扫描更快的情况5. 分区剪枝&#xff08;Partition Pruning&#xff09;6. 使用 LIMIT 和 OFFSET 的技巧7. 使用 EXPLAIN 语句分析查询性…

免费获取独立ChatGPT账户!!

GPT对于每个科研人员已经成为不可或缺的辅助工具&#xff0c;不同的研究领域和项目具有不同的需求。如在科研编程、绘图领域&#xff1a;1、编程建议和示例代码: 无论你使用的编程语言是Python、R、MATLAB还是其他语言&#xff0c;都可以为你提供相关的代码示例。2、数据可视化…

算法、数据结构、计算机系统、数据库MYSQL、概率论、数学实验MATLAB、数学建模、马原、英语、杂项、QT项目

算法 冒号表达式 &#xff08;condition&#xff09;&#xff1f;x&#xff1a;y 可以三个条件 以此类推 &#xff08;condition1&#xff09;&#xff1f;x&#xff1a;&#xff08;condition2&#xff09;&#xff1f;y&#xff1a;z 判断三角形最简单的办法 bool canFormTr…

stm32---定时器输入捕获

一、输入捕获介绍 在定时器中断实验章节中我们介绍了通用定时器具有多种功能&#xff0c;输入捕获就是其中一种。 STM32F1除了基本定时器TIM6和TIM7&#xff0c;其他定时器都具有输入捕获功能 。输入捕获可以对输入的信号的上升沿&#xff0c;下降沿或者双边沿进行捕获&#xf…

基于Python flask 的某招聘网站爬虫,招聘岗位可视化系统

招聘信息可视化系统 一、介绍 原文地址 今天为大家带来的是Python基于Flask的招聘信息爬取&#xff0c;招聘岗位分析、招聘可视化系统。 此系统是一个实时分析招聘信息的系统&#xff0c;应用Python爬虫、Flask框架、Echarts、VUE等技术实现。 本项目利用 Python 从某招聘网…

【SpringBoot】生成二维码、在图片中嵌入二维码

背景 说明&#xff1a;本文章是介绍&#xff0c;在一张背景图片中嵌入生成的二维码和中文文字。 用处&#xff1a;比如活动分享二维码的时候&#xff0c;提供一张背景图&#xff0c;然后在背景图中嵌入二维码等。 注意&#xff1a;二维码和文字的位置需要你自行调整。 一、依赖…

两种风格的纯CSS3加载动画

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>加载动画</title><style>.loader {w…

已解决 TypeError: Fetch argument None has invalid type <class ‘NoneType‘>

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页: &#x1f405;&#x1f43e;猫头虎的博客&#x1f390;《面试题大全专栏》 &#x1f995; 文章图文并茂&#x1f996…

深入学习 Redis Cluster - 基于 Docker、DockerCompose 搭建 Redis 集群,处理故障、扩容方案

目录 一、基于 Docker、DockerCompose 搭建 Redis 集群 1.1、前言 1.2、编写 shell 脚本 1.3、执行 shell 脚本&#xff0c;创建集群配置文件 1.4、编写 docker-compose.yml 文件 1.5、启动容器 1.6、构建集群 1.7、使用集群 1.8、如果集群中&#xff0c;有节点挂了&am…

Perl区分文件换行符类型

背景 在Windows上使用Perl判断文件时何种换行符时&#xff0c;处理CR LF类型的换行符时&#xff0c;也识别成了LF。 思路 Windows上的换行是 CRLF , Unix上是 LF , Mac CR在Windows平台使用Perl读取文件创建文件句柄时&#xff0c;未对file handler设置binmode&#xff0c;了…

24.Xaml ListView控件-----显示数据

1.运行效果 2.运行源码 a.Xaml源码 <Window x:Class="testView.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.mic…