Kotlin协程在android中的使用总结

认识协程

引用官方的一段话

协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。
协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码

简单概括:以同步的方式去编写异步执行的代码。协程是依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的。

协程的实现,会用到线程,但是使用协程不用类比线程,跟线程是不同的概念。

Android项目引入协程

  1. 在项目 根build.gradle - buildscript - dependencies下引入kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  1. 在项目各module - build.gradle - dependencies 中引入kotlin库和协程库
//kotlin库
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
//协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
//协程android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"

做完以上步骤,就可以在项目中使用协程了。

协程的基础用法

override fun onCreate(savedInstanceState: Bundle?) {//do someThinglifecycleScope.launch {//非及时任务延后处理delay(2000)getHenanRegion()getCommentHintText()}
}

上面就是,kotlin的简单使用,示例中延迟了2s,处理非及时响应任务,没有阻塞主线程。而且没有借助Handler, 线程.

协程,离不开以下部分

  • CoroutineScope(协程作用域),如示例的lifecycleScope
  • 启动函数(协程作用域的扩展函数),如示例launch .
  • 挂起函数,一般是网络请求耗时操作,或者延时功能性函数 如示例中delay(2000) 负责延时2s, 但不阻塞主线程。

明白这些内容就可以写协程代码了。

协程作用域

协程作用域(Coroutine Scope)是协程运行的作用范围。CoroutineScope定义了新启动的协程作用范围,同时会继承了他的coroutineContext自动传播其所有的 elements和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。

协程的启动函数

示例使用了launch函数,即为启动函数。表示开始执行协程,即{}内部分。launch是最常用的启动函数,另外还有asyncrunBlocking等。

  1. runBlocking:T 启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T,就是你协程体中最后一行是什么类型,最终返回的是什么类型T就是什么类型。
  2. launch:Job 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job
  3. async:Deferred<T> 启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。返回值泛型TrunBlocking类似都是协程体最后一行的类型。Deferred继承自Job,我们可以把它看做一个带有返回值的Job.

挂起函数

suspend是协程的关键字,表示这个一个挂起函数,每一个被suspend饰的方法只能在suspend方法或者在协程中调用。
一般耗时任务,或者功能性任务放在挂起函数中。

如网络请求

	@FormUrlEncoded@POST("/login/user/xxx")suspend fun userLogin(@Field("cellphone") cellphone: String,@Field("captcha") captcha: String): AppResult<LoginResult>

协程调度器

协程调度器CoroutineDispatcher 是用来指定协程执行所在的线程或者调度器。
Kotlin 协程库提供了几个预定义的调度器,在封装单例类Dispatchers中,如 Dispatcher.Main(用于UI线程)、Dispatcher.IO(用于I/O密集型任务)和 Dispatcher.Default(用于CPU密集型任务)。通过选择合适的调度器,我们可以控制协程的执行环境,实现线程管理。

使用,在启动函数中,传入对应的调度器即可。如下面代码:

lifecycleScope.launch(Dispatchers.IO) {//放在IO 线程中,处理耗时任务doCopyFile(src, dst)withContext(Dispatchers.Main) {// 编辑图片}}

协程执行中间要切换线程怎么办?我们可以再次调用launch启动方法(不推荐),但是如果来回切换线程的次数过多,就会出现地狱式回调。我们也可以使用withContext.

withContext是一个顶级函数,使用withContext函数来改变协程的上下文,而仍然驻留在相同的协程中,同时withContext还携带有一个泛型T返回值。

如上述示例,如果我们想拷贝文件完成,在主线程做些使用,就可以这样写

lifecycleScope.launch(Dispatchers.IO) {//拷贝文件,放在IO 线程中,处理耗时任务doCopyFile(src, dst)withContext(Dispatchers.Main) {// 刷新美颜素材,放在主线程showBeautyView()}}

使用总结

至此,协程的三大件(CoroutineScopeDispatcherssuspend关键字)已经介绍完了。这三大件共同构成了Kotlin协程的核心机制,使得开发者能够编写高效、易于理解和维护的异步代码。简单的协程应用应该不成问题了。

协程的进阶知识

协程上下文

CoroutineContext即协程上下文。CoroutineContext是一个非常核心的概念,它代表了协程执行的环境,包括协程的执行者(Dispatcher)、协程的父子关系、协程的元数据等。

它是一个包含了用户定义的一些各种不同元素的Element对象集合。其中主要元素是Job协程调度器CoroutineDispatcher、还有包含协程异常CoroutineExceptionHandler拦截器ContinuationInterceptor协程名CoroutineName等。这些数据都是和协程密切相关的,每一个Element都一个唯一key。划重点,后面的主要方法都是依据此特性。

CoroutineContext主要方法

plus方法
plus有个关键字operator表示这是一个运算符重载的方法,类似List.plus的运算符,这样我们可以通过+操作符用于合并两个CoroutineContext,创建一个新的CoroutineContext,这个新上下文包含了左右两边Context的所有元素。
这里的元素主要是指协程相关的属性,如协程调度器(Dispatcher)、协程范围(CoroutineScope)、协程名称协程的父母关系等。当两个Context中有重复的元素(如调度器),后者将会覆盖前者,因为Context合并遵循 右优先 原则。

val baseContext = CoroutineContext(Dispatchers.Default)
//newContext将会使用Dispatchers.Main作为其调度器,
//因为它在合并过程中覆盖了之前的Dispatchers.Default。
val newContext = baseContext + Dispatchers.Main

实际应用在实际开发中,+操作符经常用于在启动协程时,通过扩展当前的上下文来指定额外的属性,比如改变调度器、添加协程的名称以便于调试等。如下示例:

launch(Dispatchers.IO + coroutineContext + CoroutineName("MyCoroutine")) {// 协程逻辑
}

通过这种方式,你可以灵活地组合和定制每个协程的执行环境,满足特定的执行需求。

get方法

CoroutineContext中查询指定类型的元素。如果找到了匹配的元素,它会返回该元素的实例;如果没有找到,则返回null。这使得开发者能够根据需要检查协程上下文中是否存在特定的组件。

val context = Dispatchers.IO + CoroutineName("Coroutine1")
// 查询CoroutineName元素
val nameElement = context.get<CoroutineName>()
minusKey方法

从当前的CoroutineContext中移除(排除)指定类型的元素。
调用minusKey,键(Key)参数跟上述+get一致,执行minusKey后,返回一个新的CoroutineContext,这个新的上下文是原上下文的一个子集,不包含被指定键所对应的元素。原CoroutineContext本身保持不变,因为它是不可变的。

fold方法

fold方法是一种用于将协程上下文中的元素聚合为单个值的高阶函数。这个方法源自于函数式编程的概念,其基本思想是在一个累积值上应用一个二元操作,遍历上下文中所有元素,最终得到一个结果值。
CoroutineContext的场景中,它允许你对上下文中的每个元素执行某种操作,并将这些操作的结果合并成一个最终结果。

//fold定义
//initial: 这是聚合操作的初始值,决定了最终结果的类型
//operation: 这是一个 lambda 函数,接收两个参数:一个是当前的累积值(从initial开始),
//  另一个是正在处理的CoroutineContext.Element。这个函数定义了如何将当前元素与累积值结合,
//  返回一个新的累积值。
public inline fun <R> CoroutineContext.fold(initial: R, operation: (R, Element) -> R): R

fold示例: fold从初始值0开始,对于上下文中每个MyElement元素,它将当前累计值与该元素的value相加,最终得到所有MyElement的值之和。

class MyElement(val value: Int) : CoroutineContext.Elementval context = EmptyCoroutineContext +MyElement(1) +MyElement(2) +MyElement(3)// 使用fold方法计算所有MyElement的value之和
val sum = context.fold(0) { acc, element ->if (element is MyElement) acc + element.value else acc
}
println("Sum of values: $sum") // 输出: Sum of values: 6

CoroutineContextfold方法提供了一种强大的方式来处理和聚合协程上下文中的信息,它允许开发者以声明式的方式表达对上下文的复杂操作,提高了代码的可读性和灵活性。

协程作用域

协程作用域CoroutineScope为协程定义作用范围,每个协程生成器launchasync等都是CoroutineScope的扩展函数,并继承了它的coroutineContext自动传播其所有Element和取消。
之前我们都是使用GlobalScope,或者android中的 LifeCycleScopeViewModelScope这些,我们能不能自己定义 协程作用域呢?

先看下CoroutineScope 的相关函数。

public interface CoroutineScope {public val coroutineContext: CoroutineContext
}
//CoroutineScope也重载了plus方法,通过+号来新增或者修改我们CoroutineContext协程上下文中的Element
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =ContextScope(coroutineContext + context)public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)public object GlobalScope : CoroutineScope {override val coroutineContext: CoroutineContextget() = EmptyCoroutineContext
}
//CoroutineScope的构造函数,参数中没有job会新建一个job
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =ContextScope(if (context[Job] != null) context else context + Job())

看作用域的构造函数,参数只有一个CoroutineContext,也就是我们上面介绍的部分,自定义协程作用域,也就是是定义CoroutineContext.

自定义作用域,示例

val scope = CoroutineScope(Dispatchers.IO + CoroutineName("self define"))
scope.launch {Log.i("scope", "i am in a scope.${coroutineContext[CoroutineName]}")delay(2000)Log.i("scope", "i am in a scope, after do something")
}

协程异常的处理

执行一段代码,可以会抛出异常,如果我们没有try...catch,程序将停止执行。协程也是一样,出现了异常,如果没有处理,也会导致协程退出,甚至崩溃。协程的异常处理,使用CoroutineExceptionHandler捕获,它也是CoroutineContext的一种。当然我们可以使用+拼接。

下面我们一步步深入,对协程的异常处理

  • 最简单的不做任何异常处理,这个很好理解,和普通程序类型将导致崩溃。
  • 使用CoroutineExceptionHandler捕获,默认情况下,它会将异常传播到它的父级,父级会取消其余的子协程,同时取消自身的执行。可以这样理解,对照普通代码,相当于我们在父级作用域有一个try...catch当出现异常时,会走到异常处理代码块,其他逻辑都不执行了,父级作用域和子级作用域都不会执行。
  • 当出现异常时,如果我不想影响父级作用域,和兄弟作用域怎么办呢,只需要将当前作用域Job替换为SupervisorJob即可。这时对比普通代码,相当于我们在当前作用域加了一个try...catch,其他作用域逻辑正常执行。

默认情况下,当协程因出现异常失败时,它会将异常传播到它的父级,父级会取消其余的子协程,同时取消自身的执行。最后将异常在传播给它的父级。当异常到达当前层次结构的根,在当前协程作用域启动的所有协程都将被取消。

private fun testCoroutineSupervisorJob() {val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->Log.d("exceptionHandler", "-------${coroutineContext[CoroutineName]} $throwable")}val coroutineScope = CoroutineScope(CoroutineName("coroutineScope"))coroutineScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {val scope2 = launch(SupervisorJob()+ CoroutineName("scope2") + exceptionHandler) {Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")throw  NullPointerException("空指针")}val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {scope2.join()Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")delay(2000)Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")}scope2.join()Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")scope3.join()Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")}}

上述代码,输出log

 1--------- CoroutineName(scope2)-------CoroutineName(scope2) java.lang.NullPointerException: 空指针2--------- CoroutineName(scope3)4--------- CoroutineName(scope1)3--------- CoroutineName(scope3)5--------- CoroutineName(scope1)

如果scope2为普通Job,走到异常处,代码将不再执行。不会输出2,4,3,5 大家可以试下。

SupervisorJob异常隔离性:SupervisorJob在协程作用域中,提供了异常隔离机制。如果作用域下的某个协程抛出了异常,它只会取消自己,而不会导致整个作用域或其它协程被取消。这对于构建健壮态系统特别关键,允许部分失败而不影响全局。

协程还可以使用 supervisorScope函数,效果同SupervisorJob写法不同。如下,log同上。

private fun testCoroutineSupervisorJob() {val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->Log.d("exceptionHandler", "-------${coroutineContext[CoroutineName]} $throwable")}val coroutineScope = CoroutineScope(CoroutineName("coroutineScope"))coroutineScope.launch(Dispatchers.Main + CoroutineName("scope1") + exceptionHandler) {supervisorScope {val scope2 = launch(CoroutineName("scope2") + exceptionHandler) {Log.d("scope", "1--------- ${coroutineContext[CoroutineName]}")throw NullPointerException("空指针")}val scope3 = launch(CoroutineName("scope3") + exceptionHandler) {scope2.join()Log.d("scope", "2--------- ${coroutineContext[CoroutineName]}")delay(2000)Log.d("scope", "3--------- ${coroutineContext[CoroutineName]}")}scope2.join()Log.d("scope", "4--------- ${coroutineContext[CoroutineName]}")scope3.join()Log.d("scope", "5--------- ${coroutineContext[CoroutineName]}")}}}

协程在android中的应用

Android开发中,我们常用到 lifecycleScope, viewModelScope
lifecycleScopeKotlin协程库为Android应用特别设计的一个特性,它将协程的生命周期与ActivityFragment的生命周期绑定在一起,确保协程在相应的组件(如ActivityFragment)销毁时能够自动取消,从而避免内存泄漏和资源浪费。
ActivityFragment中使用lifecycleScope启动的协程,无需手动管理协程的取消逻辑,因为当组件生命周期状态变化时,lifecycleScope会自动处理协程的取消逻辑。
lifecycleScope能够感知ActivityFragment的生命周期变化,当组件不再活动 ON_DESTROY时,协程会被取消。lifecycleScope提供了一些方法,可以在不同生命周期调用,如launchWhenCreated launchWhenStarted launchWhenResumed

viewModelScope为在ViewModel内部启动的协程定义了一个明确的作用域。这意味着在ViewModel生命周期内启动的协程将遵循ViewModel的生存周期,当ViewModel被清除时,所有相关的协程也将被取消,有助于资源管理。
通过viewModelScope,开发者无需手动处理协程的取消逻辑,ViewModel的生命周期会自动管理协程的生命周期,使得代码更简洁、易维护。

参考资料

史上最详Android版kotlin协程入门进阶实战
Android Kotlin协程指南

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

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

相关文章

JDK、JRE、编译指令和垃圾回收机制详解

JDK 全称 Java SE Development Kit (Java 开发工具包) JVM虚拟机&#xff1a;Java运行的地方 核心类库&#xff1a;Java提前编好的东西 开发工具&#xff1a; javac,java,jdb,jhat javac:Java编译器&#xff0c;用于将Java源代码编译成Java字节码文件(.class)。 java: java…

[STM32-HAL库]AS608-指纹识别模块-STM32CUBEMX开发-HAL库开发系列-主控STM32F103C8T6

目录 一、前言 二、详细步骤 1.光学指纹模块 2.配置STM32CUBEMX 3.程序设计 3.1 输出重定向 3.2 导入AS608库 3.3 更改端口宏定义 3.4 添加中断处理部分 3.5 初始化AS608 3.6 函数总览 3.7 录入指纹 3.8 验证指纹 3.9 删除指纹 3.10 清空指纹库 三、总结及资源 一、前言 …

[力扣题解] 797. 所有可能的路径

题目&#xff1a;797. 所有可能的路径 思路 深度搜索 代码 // 图论哦!class Solution { private:vector<vector<int>> result;vector<int> path;// x : 当前节点void function(vector<vector<int>>& graph, int x){int i;// cout <&l…

解决鼠标滚动时element-ui日期选择器错位的问题

解决方案&#xff1a;监听鼠标滚动事件&#xff0c;在鼠标滚动时隐藏element-ui日期选择器下拉框 1、先在util文件夹下创建个hidePicker.js文件&#xff0c;代码如下&#xff1a; let el nullconst fakeClickOutSide () > {const SELECTWRAP_BODY document.body // bod…

Day37 贪心算法part04

LC860柠檬水找零(未掌握) 未掌握分析&#xff1a;20的时候找零卡住&#xff0c;同时贪心思路就想了很久 当bill[i]20的时候&#xff0c;我们有两种找零范式&#xff0c;找零10、5和找零三个5&#xff0c;优先找零10、5&#xff0c;因为三个5是可以替代10、5的情况的&#xff0…

Nebula街机模拟器 Mac移植版(400+游戏roms)汉化版

nebula星云模拟器是电脑上最热门的街机游戏模拟器之一&#xff0c;玩家可以通过这个小巧的模拟器软件进行多款经典街机游戏启动和畅玩&#xff0c;本次移植的包含400多款游戏roms&#xff0c;经典的三国志、三国战纪、拳皇、街霸、合金弹头、1941都包含在内。 下载地址&#xf…

CompletableFuture的主要用途是什么?

CompletableFuture 的主要用途是为复杂的异步编程模型提供一种更简单&#xff0c;更具可读性的方式。它主要用于以下几个方面&#xff1a; 非阻塞计算&#xff1a;CompletableFuture 为处理高延迟的计算任务提供了非阻塞的解决方案。你可以启动一个计算任务&#xff0c;而不需要…

前端 CSS 经典:好看的标题动画

前言&#xff1a;好看的标题动画实现。 效果&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><…

YOLOv5 AssertionError: “XXX” acceptable suffix is [‘.pt‘]

使用终端训练YOLOv5模型报错&#xff0c;原命令为&#xff1a; “python train.py --img 640 --batch 1 --epochs 25 --data "C:\Users\GRT\PycharmProjects\yolov5-7.0\animal_training\dataset.yaml " --weights “C:\Users\GRT\PycharmProjects\yolov5-7.0\MyFunc…

组播协议简介

一、组播协议介绍 组播协议是一种网络通信协议&#xff0c;它允许一个发送者同时向多个接收者发送数据。以下是组播协议的一些特点&#xff1a; 高效性&#xff1a;组播协议可以有效地利用网络带宽&#xff0c;因为它只需要发送一份数据副本&#xff0c;就可以被多个接收者同…

蓝桥楼赛第30期-Python-第三天赛题 从参数中提取信息题解

楼赛 第30期 Python 模块大比拼 提取用户输入信息 介绍 正则表达式&#xff08;英文为 Regular Expression&#xff0c;常简写为regex、regexp 或 RE&#xff09;&#xff0c;也叫规则表达式、正规表达式&#xff0c;是计算机科学的一个概念。 所谓“正则”&#xff0c;可以…

docker swarm多主机之间的端口无法访问,但能ping通 问题排查及解决

已排查&#xff1a;1.ufw status 防火墙已关闭 2.selinux已关闭 3.netstat -ntpl :::8088 未限制ip 问题&#xff1a;docker swarm多主机之间的端口无法访问&#xff0c;但能ping通&#xff0c;同一主机下的端口也可以访问。 原因&#xff1a;docker overlay网络内部使用…

【Linux取经路】初识线程——线程控制

文章目录 一、什么是线程&#xff1f;1.1 Linux 中线程该如何理解&#xff1f;1.2 如何理解把资源分配给线程&#xff1f;1.2.1 虚拟地址到物理地址的转换 1.3 线程 VS 进程1.3.1 线程为什么比进程更轻量化&#xff1f;1.3.2 线程的优点1.3.3 线程缺点1.3.4 线程异常1.3.5 线程…

关于基础的流量分析(1)

1.对于流量分析基本认识 1&#xff09;简介&#xff1a;网络流量分析是指捕捉网络中流动的数据包&#xff0c;并通过查看包内部数据以及进行相关的协议、流量分析、统计等来发现网络运行过程中出现的问题。 2&#xff09;在我们平时的考核和CTF比赛中&#xff0c;基本每次都有…

MySQL用户管理操作

用户权限管理操作 DCL语句 一.用户管理操作 MySQL软件内部完整的用户格式&#xff1a; 用户名客户端地址 admin1.1.1.1这个用户只能从1.1.1.1的客服端来连接服务器 admin1.1.1.2这个用户只能从1.1.1.2的客服端来连接服务器 rootlocal host这个用户只能从服务器本地进行连…

Prompt - 流行的10个框架

转载自&#xff1a;https://juejin.cn/post/7287412759050289212 文章目录 1、ICIO框架2、CRISPE框架3、BROKE框架4、CREATE框架5、TAG框架6、RTF框架7、ROSES框架8、APE框架9、RACE框架10、TRACE框架 测试用例 为了看到不同的Prompt框架效果&#xff0c;本文定义一个统一的测…

ACM实训

【碎碎念】继续搞习题学习&#xff0c;今天完成第四套的ABCD&#xff0c;为下一周挤出时间复习&#xff0c;加油 Digit Counting 问题 法希姆喜欢解决数学问题。但有时解决所有的数学问题对他来说是一个挑战。所以有时候他会为了解决数学难题而生气。他拿起一支粉笔&#xff…

Java面试八股之进程和线程的区别

Java进程和线程的区别 定义与作用&#xff1a; 进程&#xff1a;在操作系统中&#xff0c;进程是程序执行的一个实例&#xff0c;是资源分配的最小单位。每个进程都拥有独立的内存空间&#xff0c;包括代码段、数据段、堆空间和栈空间&#xff0c;以及操作系统分配的其他资源…

工厂模式(简单工厂模式+工厂模式)

工厂模式的目的就是将对象的创建过程隐藏起来&#xff0c;从而达到很高的灵活性&#xff0c;工厂模式分为三类&#xff1a; 简单工厂模式工厂方法模式抽象工厂模式 在没有工厂模式的时候就是&#xff0c;客户需要一辆马车&#xff0c;需要客户亲自去创建一辆马车&#xff0c;…

PDF之Blend Mode(混合模式)BM(对应OFD的BlendMode)

Blend Mode&#xff08;混合模式&#xff09;用于定义对象与背景或其他对象之间的颜色混合方式。PDF支持多种混合模式&#xff0c;常见的混合模式包括&#xff1a; Normal&#xff1a;正常混合模式&#xff0c;将对象颜色直接叠加在背景上。 Multiply&#xff1a;乘法混合模式…