认识协程
引用官方的一段话
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。
协程是一种并发设计模式,您可以在Android平台上使用它来简化异步执行的代码
简单概括:以同步的方式去编写异步执行的代码。协程是依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的。
协程的实现,会用到线程,但是使用协程不用类比线程,跟线程是不同的概念。
Android项目引入协程
- 在项目
根build.gradle
-buildscript
-dependencies
下引入kotlin
kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- 在项目各
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
是最常用的启动函数,另外还有async
,runBlocking
等。
runBlocking:T
启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T
,就是你协程体中最后一行是什么类型,最终返回的是什么类型T
就是什么类型。launch:Job
启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope
)中才能调用,返回值是一个Job
。async:Deferred<T>
启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope
)中才能调用。以Deferred
对象的形式返回协程任务。返回值泛型T
同runBlocking
类似都是协程体最后一行的类型。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()
}
}
使用总结
至此,协程的三大件(CoroutineScope
、Dispatchers
、suspend关键字
)已经介绍完了。这三大件共同构成了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
CoroutineContext
的fold
方法提供了一种强大的方式来处理和聚合协程上下文中的信息,它允许开发者以声明式的方式表达对上下文的复杂操作,提高了代码的可读性和灵活性。
协程作用域
协程作用域CoroutineScope
为协程定义作用范围,每个协程生成器launch
、async
等都是CoroutineScope
的扩展函数,并继承了它的coroutineContext
自动传播其所有Element
和取消。
之前我们都是使用GlobalScope
,或者android中的 LifeCycleScope
,ViewModelScope
这些,我们能不能自己定义 协程作用域呢?
先看下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
lifecycleScope
是Kotlin
协程库为Android
应用特别设计的一个特性,它将协程的生命周期与Activity
或Fragment
的生命周期绑定在一起,确保协程在相应的组件(如Activity
或Fragment
)销毁时能够自动取消,从而避免内存泄漏和资源浪费。
在Activity
或Fragme
nt中使用lifecycleScope
启动的协程,无需手动管理协程的取消逻辑,因为当组件生命周期状态变化时,lifecycleScope
会自动处理协程的取消逻辑。
lifecycleScope
能够感知Activity
或Fragment
的生命周期变化,当组件不再活动 ON_DESTROY
时,协程会被取消。lifecycleScope
提供了一些方法,可以在不同生命周期调用,如launchWhenCreated
launchWhenStarted
launchWhenResumed
viewModelScope
为在ViewModel
内部启动的协程定义了一个明确的作用域。这意味着在ViewModel
生命周期内启动的协程将遵循ViewModel
的生存周期,当ViewModel
被清除时,所有相关的协程也将被取消,有助于资源管理。
通过viewModelScope
,开发者无需手动处理协程的取消逻辑,ViewModel
的生命周期会自动管理协程的生命周期,使得代码更简洁、易维护。