Kotlin协程在android中的使用总结

认识协程

引用官方的一段话

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

协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码

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

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

Android项目引入协程

  1. 在项目 根build.gradle - buildscript - dependencies下引入kotlin
kotlin 复制代码
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  1. 在项目各module - build.gradle - dependencies 中引入kotlin库和协程库
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"

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

协程的基础用法

kotlin 复制代码
override fun onCreate(savedInstanceState: Bundle?) {
	//do someThing
	lifecycleScope.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方法或者在协程中调用。

一般耗时任务,或者功能性任务放在挂起函数中。

如网络请求

kotlin 复制代码
	@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密集型任务)。通过选择合适的调度器,我们可以控制协程的执行环境,实现线程管理。

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

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

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

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

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

kotlin 复制代码
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合并遵循 右优先 原则。

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

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

kotlin 复制代码
launch(Dispatchers.IO + coroutineContext + CoroutineName("MyCoroutine")) {
    // 协程逻辑
}

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

get方法

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

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

从当前的CoroutineContext中移除(排除)指定类型的元素。

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

fold方法

fold方法是一种用于将协程上下文中的元素聚合为单个值的高阶函数。这个方法源自于函数式编程的概念,其基本思想是在一个累积值上应用一个二元操作,遍历上下文中所有元素,最终得到一个结果值。

CoroutineContext的场景中,它允许你对上下文中的每个元素执行某种操作,并将这些操作的结果合并成一个最终结果。

kotlin 复制代码
//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的值之和。

kotlin 复制代码
class MyElement(val value: Int) : CoroutineContext.Element

val 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 的相关函数。

kotlin 复制代码
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: CoroutineContext
        get() = EmptyCoroutineContext
}
//CoroutineScope的构造函数,参数中没有job会新建一个job
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

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

自定义作用域,示例

kotlin 复制代码
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,其他作用域逻辑正常执行。

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

kotlin 复制代码
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

kotlin 复制代码
 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同上。

kotlin 复制代码
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协程指南

相关推荐
ghie90902 小时前
Kotlin中Lambda表达式和匿名函数的区别
java·算法·kotlin
潜龙95273 小时前
第3.2.3节 Android动态调用链路的获取
android·调用链路
追随远方4 小时前
Android平台FFmpeg音视频开发深度指南
android·ffmpeg·音视频
撰卢5 小时前
MySQL 1366 - Incorrect string value:错误
android·数据库·mysql
恋猫de小郭5 小时前
Flutter 合并 ‘dot-shorthands‘ 语法糖,Dart 开始支持交叉编译
android·flutter·ios
牛马程序小猿猴5 小时前
15.thinkphp的上传功能
android
林家凌宇6 小时前
Flutter 3.29.3 花屏问题记录
android·flutter·skia
时丶光6 小时前
Android 查看 Logcat (可纯手机方式 无需电脑)
android·logcat
血手人屠喵帕斯6 小时前
事务连接池
android·adb
恋猫de小郭7 小时前
React Native 前瞻式重大更新 Skia & WebGPU & ThreeJS,未来可期
android·javascript·flutter·react native·react.js·ios