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协程指南

相关推荐
1024小神39 分钟前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛1 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法1 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter3 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快4 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl4 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江4 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-5 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记
佛系小嘟嘟5 小时前
Android Studio不显示需要的tag日志解决办法《All logs entries are hidden by the filter》
android·ide·android studio
mariokkm5 小时前
Django一分钟:django中收集关联对象关联数据的方法
android·django·sqlite