1.协程可以同步的方式去编写异步执行的代码
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱
协程在写法上和普通的顺序代码类似,同步的方式去编写异步执行的代码
GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
val result = userApi.getUserSuspend("suming")//网络请求(IO 线程)//耗时阻塞的操作交给IO线程
tv_name.text = result?.name //更新 UI(主线程) // 恢复到主线程
}
2.协程可以降低异步程序的设计复杂度。
3.在不同的线程之间切换,保证线程安全。
一个线程上可以一个或多个协程。
4. 处理耗时任务(比如网络请求、解析JSON
数据、从数据库中进行读写操作等)
kotlin的协程实现分为了两个层次:
基础设施层(协程标准库) | 标准库的协程API,主要对协程提供了概念和语义上最基本的支持; |
业务框架层 kotlin.coroutines | 协程的上层框架支持,基于协程标准库实现的封装,也是我们日常开发使用的协程扩展库。 |
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
}
dependencies {
//协程标准库
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
//协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
//协程Android支持库,提供安卓UI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
协程的最核心的原理就是能够把函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。
runBlocking:T
一般在项目中不会使用,主要是为main函数和测试设计的。
launch
创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job
对象。这是最常用的用于启动协程的方式。
1. GlobalScope.launch() 全局的协程 不推荐这种用法
2.CoroutineScope.launch() 启动一个新的协程而不阻塞当前线程,并返回对协程的引用作为一个Job。
组件销毁时,协程一并销毁 在应用中最推荐使用的协程使用方式。
async
:
1.创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer
对象,可通过调用Deffer.await()
方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。
注意:await()
不能在协程之外调用,因为它需要挂起直到计算完成,而且只有协程可以以非阻塞的方式挂起。所以把它放到协程中。
2. 当在协程作用域中使用async
函数时可以创建并发任务:
注意:⑴如果Deferred
不执行await()
则async
内部抛出的异常不会被logCat
或tryCatch
捕获, 但是依然会导致作用域取消和异常崩溃; 但当执行await时异常信息会重新抛出。
⑵惰性并发,如果将async
函数中的启动模式设置为CoroutineStart.LAZY
懒加载模式时则只有调用Deferred
对象的await
时(或者执行 async.satrt()
)才会开始执行异步任务。
launch构建器:适合执行 "一劳永逸" 的工作,意思就是说它可以启动新协程而不需要结果返回;
async构建器:可启动新协程并允许您使用一个名为await的挂起函数返回result,并且支持并发。
如果使用async作为最外层协程的开启方式,它期望最终是通过调用 await 来获取结果 (或者异常),
所以默认情况下它不会抛出异常。这意味着如果使用 async启动新的最外层协程,而不使用await,它会静默地将异常丢弃。
Job
⑴Job 是门把手 协程是门 通过门把手Job 可以控制门(协程)。 Job是launch构建协程返回的一个协程任务,完成时是没有返回值的。
⑵可以把Job看成协程对象本身。协程的操作方法都在Job身上。 Job具有生命周期并且可以取消,它也是上下文元素,继承自CoroutineContext。
public interface Job : CoroutineContext.Element {
//活跃的,是否仍在执行
public val isActive: Boolean//启动协程,如果启动了协程,则为true;如果协程已经启动或完成,则为false
public fun start(): Boolean
//取消Job,可通过传入Exception说明具体原因
public fun cancel(cause: CancellationException? = null)
//挂起协程直到此Job完成
public suspend fun join()
//取消任务并等待任务完成,结合了[cancel]和[join]的调用
public suspend fun Job.cancelAndJoin()//给Job设置一个完成通知,当Job执行完成的时候会同步执行这个函数
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
}
状态 | 说明 |
---|---|
isActive |
活跃的。当Job 处于活动状态时为true ,如果Job 已经开始,但还没有完成、也没有取消或者失败,则是处于active 状态。 |
isCompleted |
已完成。当Job 由于任何原因完成时为true ,已取消、已失败和已完成Job 都是被视为完成状态。 |
isCancelled |
已退出。当Job 由于任何原因被取消时为true ,无论是通过显式调用cancel 或这因为它已经失败亦或者它的子或父被取消,都是被视为已退出状态。 |
⑶Job几个比较有用的函数:
❶ join()是一个挂起函数,它需要等待协程的执行,如果协程尚未完成,join()立即挂起,直到协程完成;如果协程已经完成,join()不会挂起,而是立即返回。
一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;
当子Job被取消或者出现异常后父Job也会被取消。具有多个子 Job 的父Job 会等待所有子Job完成(或者取消)后,自己才会执行完成。
❷ cancel(cause: CancellationException? = null) //取消Job协程,可通过传入Exception说明具体原因
❸ cancelAndJoin()// 取消任务并等待任务完成,结合了[cancel]和[join]的调用 以这个使用为主
MainScope().launch(context = Dispatchers.Main,block={
val startTime = System.currentTimeMillis()
val job = launch(context = Dispatchers.Default, block = {
var nextPrintTime = startTime
var i = 0
while (isActive) { //当子协程job是活跃状态下(isActive = true)才会打印输出, 在job子协程取消后, 非活跃状态(isActive = false) 已取消,第3和第4条数据不会被打印出来。
if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
println("job:执行打印 ${i++} ...") // job:执行打印 1 ... job:执行打印 2 ...
nextPrintTime += 500
}
}
})
delay(1200)//延迟1.2s
println("等待1.2秒后")
job.cancelAndJoin()//取消任务并等待任务完成 //挂起并调用协程,直到被取消的job完成
println("协程被取消并等待完成") // 执行此行代码
})
/**
* job:执行打印 1 ...
* job:执行打印 2 ...
* 等待1.2秒后
* 协程被取消并等待完成
* */
Deferred继承自Job,具有与Job相同的状态机制。
它是async构建协程返回的一个协程任务,可通过调用await()方法等待协程执行完成并获取结果。
不同的是Job没有结果值,Deffer有结果值。
public interface Deferred
: Job {
//等待协程执行完成并获取结果
public suspend fun await(): T
}
await()
: 等待协程执行完毕并返回结果,如果异常结束则会抛出异常;如果协程尚未完成,则挂起直到协程执行完成。T
: 这里多了一个泛型参数T
,它表示返回值类型,通过await()
函数可以拿到这个返回值。
Deferred的取消
1.async调用await()返回数据结果
MainScope().launch(context = Dispatchers.Main,block={
val asyncDeferred :Deferred = async(context = Dispatchers.IO, block = {
delay(3000) // 在给定时间内延迟协程而不阻塞线程,并在指定时间后恢复协程。你可以认为它实际上就是触发了一个延时任务,告诉协程调度系统多久之后再来执行后面的代码。
println("asyncTest")
return@async
})
val result:Unit = asyncDeferred.await()
println(" 子协程async的执行返回结果值result=$result")
})
/**
* asyncTest
* 子协程async的执行返回结果值result=kotlin.Unit
*
* */
2.在deferred.await()
之后调用deferred.cancel()
不会有任何情况发生,因为协程已经处理结束。
MainScope().launch(context = Dispatchers.Main,block={
val asyncDeferred :Deferred = async(context = Dispatchers.IO, block = {
delay(3000) // 在给定时间内延迟协程而不阻塞线程,并在指定时间后恢复协程。你可以认为它实际上就是触发了一个延时任务,告诉协程调度系统多久之后再来执行后面的代码。
println("asyncTest")
return@async
})
val result:Unit = asyncDeferred.await()
println(" 子协程async的执行返回结果值result=$result")
asyncDeferred.cancel()//asyncDeferred协程取消
})
/**
如果在deferred.await()之后调用deferred.cancel()不会有任何情况发生,因为协程已经处理结束。
asyncTest
子协程async的执行返回结果值result=kotlin.Unit
*/
3.在已取消的deferred
上调用await()
会抛出JobCancellationException
异常。因为await()
是负责在协程处理结果出来之前一直将协程挂起,如果协程被取消了那么协程就不会继续进行计算也不会有结果产生。因此,在协程取消后调用await()
会抛出JobCancellationException
异常: 因为Job
已经被取消。
MainScope().launch(context = Dispatchers.Main,block={
val asyncDeferred :Deferred = async(context = Dispatchers.IO, block = {
delay(3000) // 在给定时间内延迟协程而不阻塞线程,并在指定时间后恢复协程。你可以认为它实际上就是触发了一个延时任务,告诉协程调度系统多久之后再来执行后面的代码。
println("asyncTest")
return@async
})
asyncDeferred.cancel()//asyncDeferred协程取消
val result:Unit = asyncDeferred.await() //会抛出 JobCancellationException
println(" 子协程async的执行返回结果值result=$result")
})
CoroutineScope
协程作用域(CoroutineScope)其实就是为协程定义的作用范围,
为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用CoroutineScope的情况下启动新的协程。
它能启动新的协程,同时这个协程还具备上面所说的suspend和resume的优势。
每个协程生成器launch、async等都是CoroutineScope的扩展,并继承了它的coroutineContext自动传播其所有元素和取消。
协程作用域本质是一个接口:
public interface CoroutineScope {
//此域的上下文。Context被作用域封装,用于在作用域上扩展的协程构建器的实现。
public val coroutineContext: CoroutineContext
}
CoroutineScope
runBlocking: | 顶层函数,可启动协程。但是它会阻塞当前线程,主要用于测试。 |
GlobalScope: | 全局协程作用域,根协程, 通过GlobalScope创建的协程不会有父协程, 它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消, 在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。 |
coroutineScope : | 创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。 它是一个挂起函数,需要运行在协程内或挂起函数内。 当这个作用域中的任何一个子协程失败时,这个作用域失败,所有其他的子程序都被取消。 |
supervisorScope: | 该子协程的异常不会影响父协程,也不会影响其他子协程。 作用域本身的失败(在block或取消中抛出异常)会导致作用域及其所有子协程失败,但不会取消父协程。 |
MainScope: | 为UI组件创建主作用域。 一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main,说明它是一个在主线程执行的协程作用域, 通过cancel对协程进行取消。推荐使用。 MainScope作用域的好处就是方便地绑定到UI组件的声明周期上,在Activity销毁的时候mainScope.cancel()取消其作用域。 |
lifecycleScope |
会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。 因为 |
viewModelScope |
与ViewModel 绑定生命周期,当ViewModel 被清除时,这个作用域将被取消。推荐使用。 |
lifecycleScope viewModelScope的依赖
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
// 只有Lifecycles(没有 ViewModel 和 LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
官方框架在实现复合协程的过程中也提供了作用域,主要用于明确父子关系,以及取消或者异常处理等方面的传播行为。该作用域分为以下三种:
顶级作用域 | 其就为最顶级,没有父协程 作用域 | |
协同作用域 | (父)协程中启动新的(子)协程, 子协程所在的作用域默认为协同作用域。 子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。 |
1.父协程被取消,则所有子协程均被取消。 2.父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。 3.子协程会继承父协程的协程上下文中的元素,如果自身有相同 |
主从作用域 | (父)协程中启动新的(子)协程, (子)协程作用域出现的异常,不会将异常向上传递给父协程。 | 1.父协程被取消,则所有子协程均被取消。 2.父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完 3.子协程会继承父协程的协程上下文中的元素,如果自身有相同 |
CoroutineDispatcher
调度器也属于协程上下文 context: CoroutineContext
协程的在哪挂起 什么时候恢复,为什么能切换线程,这因为调度器的作用
协程只有在挂起点的位置才能进行调度
调度的本质就是解决挂起点恢复之后的协程逻辑在哪里运行的问题。
调度器模式 | 名称 | 说明 | 适用场景 |
context=
|
|
默认调度器,非主线程。CPU 密集型任务调度器,适合处理后台计算。 |
通常处理一些单纯的计算任务,或者执行时间较短任务比如:Json 的解析,数据计算等。 |
context=
|
|
|
调度程序是单线程的,通常用于UI 交互,刷新等。 |
context=
|
|
一个不局限于任何特定线程的协程调度程序,即非受限调度器。 | 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。 |
context=
|
|
IO 调度器,非主线程,执行的线程是IO 线程。 |
适合执行IO 相关操作,比如:网络处理,数据库操作,文件读写等。 |
//创建一个在主线程执行的协程作用域
MainScope().launch(context = Dispatchers.Main, block = {
println("MainScope Thread: ${Thread.currentThread().name}") // MainScope Thread: main
println(" MainScope当前协程的调用者对象this=$this") // MainScope当前协程的调用者对象this=StandaloneCoroutine{Active}@3457c62
launch(context = Dispatchers.Main, block = {
println("Dispatchers.Main Thread: ${Thread.currentThread().name}") //Dispatchers.Main Thread: main
println(" Dispatchers.Main 当前协程的调用者对象this=$this") // Dispatchers.Main 当前协程的调用者对象this=StandaloneCoroutine{Active}@d02fcae
})
launch(context = Dispatchers.Default, block = {
println("Dispatchers.Default Thread: ${Thread.currentThread().name}") //Dispatchers.Default Thread: DefaultDispatcher-worker-1
println(" Dispatchers.Default 当前协程的调用者对象this=$this") // Dispatchers.Default 当前协程的调用者对象this=StandaloneCoroutine{Active}@64801f3
})
launch(context = Dispatchers.Unconfined, block = {
println("Dispatchers.Unconfined Thread: ${Thread.currentThread().name}") // Dispatchers.Unconfined Thread: main
println(" Dispatchers.Unconfined 当前协程的调用者对象this=$this") // Dispatchers.Unconfined 当前协程的调用者对象this=StandaloneCoroutine{Active}@2330eb0
})
launch(context = Dispatchers.IO, block = {
println("Dispatchers.IO Thread: ${Thread.currentThread().name}") // Dispatchers.IO Thread: DefaultDispatcher-worker-1
println(" Dispatchers.IO 当前协程的调用者对象this=$this") // Dispatchers.IO 当前协程的调用者对象this=StandaloneCoroutine{Active}@a571129
})
})
在 Andorid 开发中,我们常常在子线程中请求网络获取数据,然后切换到主线程更新UI。官方为我们提供了一个withContext
顶级函数,在获取数据函数内,调用withContext(Dispatchers.IO)
来创建一个在IO
线程池中运行的块。您放在该块内的任何代码都始终通过IO
调度器执行。由于withContext
本身就是一个suspend
函数,它会使用协程来保证主线程安全。
由于withContext
可让在不引入回调的情况下控制任何代码行的线程池,因此可以将其应用于非常小的函数,如从数据库中读取数据或执行网络请求。一种不错的做法是使用withContext
来确保每个函数都是主线程安全的,那么可以从主线程调用每个函数。调用方也就无需再考虑应该使用哪个线程来执行函数了。您可以使用外部 withContext
来让 Kotlin 只切换一次线程,这样可以在多次调用的情况下,以尽可能避免了线程切换所带来的性能损失。
在Activity界面的主线程操作使用 withcontext
MainScope().launch(context=Dispatchers.Main,block={ //todo 开始协程:主线程
val loginRegisterResponseBean : BaseResponseBean
= withContext(context = Dispatchers.IO, block = { // todo 切换到IO 线程:网络请求
APIClient.instance.initRetrofit(WanAndroidAPI::class.java).loginActionCoroutine("Derry-vip", "123456")
})
//todo 恢复到主线程 :更新 UI
mBinding?.textView?.text=loginRegisterResponseBean.toString()
mBinding?.textView?.setTextColor(Color.MAGENTA)
})
MainScope().launch(context=Dispatchers.IO,block={ //todo 开始协程:搞到子协程里
delay(4000)
val loginRegisterResponseBean : BaseResponseBean
= withContext(context = Dispatchers.IO, block = { // todo 切换到IO 线程:网络请求
APIClient.instance.initRetrofit(WanAndroidAPI::class.java).loginActionCoroutine("Derry-vip", "123456")
})
//todo 恢复到主线程 :更新 UI
withContext(context=Dispatchers.Main, block = {
mBinding?.textView?.text=loginRegisterResponseBean.toString()
mBinding?.textView?.setTextColor(Color.CYAN)
})
})
GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
val result: User = withContext(Dispatchers.IO) {//网络请求(IO 线程)
userApi.getUserSuspend("FollowExcellence")
}
tv_title.text = result.name //更新 UI(主线程)
}
协程上下文主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。它有很多作用,包括携带参数,拦截协程执行等等。
协程上下文CoroutineContext的子类 | 作用 |
---|---|
context=Job |
协程的句柄,对协程的控制和管理生命周期。 |
context=CoroutineName |
协程的名称, 用于方便调试和定位问题: |
context=CoroutineDispatcher |
调度器,确定协程在指定的线程来执行。 |
context=CoroutineExceptionHandler |
协程异常处理器,处理未捕获的异常。 |
//TODO context: CoroutineContext =CoroutineName( val name: String) 协程上下文context: 指定协程上下文名称
GlobalScope.launch(context = CoroutineName("GlobalScope1号"),block={
println("获取当前协程对象this=$this, 当前协程名称coroutineName =$coroutineContext[CoroutineName]")
launch(context = CoroutineName("launch1号"),block={
println("获取当前协程对象this=$this, 当前协程名称coroutineName =$coroutineContext[CoroutineName]")
})
})
//TODO context: CoroutineContext = Dispatchers.IO // IO: CoroutineDispatcher 协程上下文context: 指定IO线程
GlobalScope.launch(context = Dispatchers.IO,block={
//TODO context: CoroutineContext = Dispatchers.Main // Main: CoroutineDispatcher 协程上下文context: 指定Main线程
launch(context=Dispatchers.Main,block={})
})
协程上下文context 要传递多个参数,
使用 “+”对传递的多个上下文参数进行合并组合
context : CoroutineContext= CoroutineName("GlobalScope2号")+Dispatchers.Main context : CoroutineContext= CoroutineName("launch2号")+Dispatchers.IO
GlobalScope.launch(context = CoroutineName("GlobalScope2号")+Dispatchers.Main,block={
println("获取当前协程对象this=$this,输出当前协程组合上下文context=$coroutineContext")
// 获取当前协程对象this=StandaloneCoroutine{Active}@2330eb0,输出当前协程组合上下文context=[CoroutineName(GlobalScope2号), StandaloneCoroutine{Active}@2330eb0, Dispatchers.Main]
launch(context = CoroutineName("launch2号")+Dispatchers.IO,block={
println("获取当前协程对象this=$this, 输出当前协程组合上下文context=$coroutineContext")
// 获取当前协程对象this=StandaloneCoroutine{Active}@a571129, 输出当前协程组合上下文context=[CoroutineName(launch2号), StandaloneCoroutine{Active}@a571129, Dispatchers.IO]
})
})
CoroutineStart
CoroutineStart
是一个枚举类,为协程构建器定义启动选项。在协程构建的start
参数中使用,
启动模式 | 含义 | 说明 |
---|---|---|
start: CoroutineStart = CoroutineStart.DEFAULT(常用) |
默认启动模式,立即根据它的上下文调度协程的执行 | 是立即调度,不是立即执行,DEFAULT 是饿汉式启动,launch 调用后,会立即进入待调度状态,一旦调度器 OK 就可以开始执行。如果协程在执行前被取消,其将直接进入取消响应的状态。 |
start: CoroutineStart = CoroutineStart.LAZY(常用) |
懒启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度 | 包括主动调用该协程的start 、join 或者await 等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。 |
start: CoroutineStart = CoroutineStart.ATOMIC |
类似[DEFAULT],以一种不可取消的方式调度协程的执行 | 虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行 |
start: CoroutineStart = CoroutineStart.UNDISPATCHED |
类似[ATOMIC],立即执行协程,直到它在当前线程中的第一个挂起点。 | 是立即执行,因此协程一定会执行。即使协程已经被取消,它也会开始执行,但不同之处在于它在同一个线程中开始执行。 |
⑴协程通过抛出一个特殊的异常CancellationException
来处理取消操作。在调用job.cancel
时你可以传入一个 CancellationException
实例来提供指定错误信息。子协程会通过抛出异常的方式将取消的情况通知到它的父协程。父协程通过传入的取消原因来决定是否来处理该异常。如果子协程因为CancellationException
而被取消的,那么对于父协程来说不需要进行额外操作。
public fun cancel(cause: CancellationException? = null)
该参数是可空的,如果不传参数则会使用默认的
defaultCancellationException()
作为参数。
⑵取消 一个作用域 (scope
) ,会取消所有其已创建的所有子协程。
取消作用域会取消它的所有子协程。 已取消的作用域无法再创建协程。
//创建作用域
val scope = CoroutineScope(Dispatchers.Main)
//启动一个协程
val job = scope.launch {
delay(4000)
println("执行子协程job")
}
val job2 = scope.launch {
delay(4000)
println("执行子协程job2")
}
//作用域取消 子协程 job job2 不在执行
scope.cancel()
⑶如果仅仅是因为要取消某个进行中的任务而取消其中某一个协程,
如果只取消某一个子协程,那么其他的兄弟子协程不受影响不会取消
//创建作用域
val scope = CoroutineScope(Dispatchers.Main)
//子协程job1将会被取消,而另一个子协程job2则不受任何影响
val job1 = scope.launch {
delay(4000)
println("执行子协程job1")
}
val job2 = scope.launch {
delay(4000)
println("执行子协程job2")
}
//取消单个子协程job1
job1.cancel() // 输出日志: 执行子协程job2
注意:
如果使用的是
androidx KTX
库的话,在大部分情况下都不需要创建自己的作用域,所以也就不需要负责取消它们。viewModelScope
和lifecycleScope
都是CoroutineScope
对象,它们都会在适当的时间点被取消。当ViewModel
被清除时,在其作用域内启动的协程也会被一起取消。lifecycleScope
会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏。
Job
的生命周期
一个Job
可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问Job
的属性: isActive、isCancelled 和 isCompleted。
⑴如果协程处于活跃状态,协程运行出错或者调用job.cancel()
都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true)
⑵当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true。
⑶ 协程所处理的任务不会仅仅在调用cancel
方法时就立马停止,相反,我们需要修改代码来定期检查协程是否处于活跃状态。在处理任务之前添加对协程状态的检查:
while (i < 5 && isActive) //当job是活跃状态继续执行
MainScope().launch(context = Dispatchers.Main,block={
val startTime = System.currentTimeMillis()
val job = launch(context = Dispatchers.Default, block = {
var nextPrintTime = startTime
var i = 0
// while (i < 5) { //打印前五条消息 有个bug 就是协程被取消了 但是第3 4 条依然打印了出来
while (i < 5 && isActive) { //当子协程job是活跃状态下(isActive = true)才会打印输出, 在job子协程取消后, 非活跃状态(isActive = false) 已取消,第3和第4条数据不会被打印出来。
if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
println("job:执行打印 ${i++} ...") // job:执行打印 1 ... job:执行打印 2 ...
nextPrintTime += 500
}
}
})
delay(1200)//延迟1.2s
println("等待1.2秒后")
job.cancel()
println("协程被取消")
})
输出打印:
job:执行打印 1 ...
job:执行打印 2 ...
等待1.2秒后
协程被取消
join()
& await()
的取消见上面 join( )与await()的区别
当协程被取消后会抛出CancellationException
异常,我们可以将挂起的任务放置于try…catch…finally
代码块中,catch
中捕获取消后抛出的异常,在finally
代码块中执行需要做的清理任务。
val job = GlobalScope.launch {
try {
//TODO
delay(500L)
} catch (e: CancellationException) {
print("协程取消抛出异常:$e")
} finally {
print("协程清理工作")
}
}job.cancel()//取消协程
[DefaultDispatcher-worker-1] 协程取消抛出异常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@bb81f53
[DefaultDispatcher-worker-1] 协程清理工作
如果协程被取消后需要调用挂起函数进行清理任务,可使用NonCancellable
单例对象用于withContext
函数创建一个无法被取消的协程作用域中执行。这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。
但是这个方法需要慎用,这样做风险很高,因为可能会无法控制协程的执行。
MainScope().launch(context = Dispatchers.Main,block={
val job:Job = launch(context = Dispatchers.IO, block = {
try {
delay(500L)
println("协程被取消了")
} catch (e: CancellationException) {
println("协程取消抛出异常:$e")
}finally {
withContext(NonCancellable) {
delay(100)//或者其他挂起函数
println("协程清理工作")
}
}
})
job.cancel()//取消协程
})
/**
* 协程取消抛出异常:kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@93cd052
* 协程清理工作
* */
withTimeout
函数用于指定协程的运行超时时间,如果超时则会抛出TimeoutCancellationException
,从而令协程结束运行。
withTimeout(1300) {//1300毫秒后超时抛出TimeoutCancellationException异常
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500)
}
}
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
1.try…catch
❶ 使用launch时,会直接抛出异常 崩溃 所以要将异常代码进行try..catch 就不会崩溃了
val coroutineScope:CoroutineScope = CoroutineScope(context = Dispatchers.Main)
// todo launch() 有异常会崩溃,进行try catch()
coroutineScope.launch {
try {
println("模拟抛出一个数组越界异常")
throw IndexOutOfBoundsException() //launch 抛出异常
} catch (e: Exception) {
//处理异常
println("执行catch捕捉住这个异常,不崩溃")
}
}
/**
* 模拟抛出一个数组越界异常
* 执行catch捕捉住这个异常,不崩溃
* */
❷async被用作根协程 (CoroutineScope实例或supervisorScope的直接子协程) 时不会自动抛出异常,而是在调用await()时才会抛出异常。为了捕获其中抛出的异常,可以用try…catch包裹调用await()的代码:
val coroutineScope2:CoroutineScope = CoroutineScope(context = Dispatchers.Main)
coroutineScope2.launch {
//todo 注意:这里很重要,这里async()和launch()协程是同级的,使用的都是根协程 CoroutineScope
// 协程语法规定:async被用作根协程 (CoroutineScope实例或supervisorScope的直接子协程) 时不会自动抛出异常,而是在调用await()时才会抛出异常。为了捕获其中抛出的异常,可以用try…catch包裹调用await()的代码:
try {
val deferred: Deferred = coroutineScope2.async{
println("模拟抛出一个算术运算异常")
throw ArithmeticException() //这里虽然有异常 但是不会抛出 所以程序不会崩溃
return@async
}
val result:Unit = deferred.await() // await() try catch了 程序就不会崩溃了
} catch (e: Exception) {
//处理异常
println("执行catch捕捉住这个异常,不崩溃")
}
}
/**
* 模拟抛出一个算术运算异常
执行catch捕捉住这个异常,不崩溃
* */
❸ 根协程CoroutineScope的直接子协程是launch()【不是async()】, launch()的子协程是async(),
协程语法规定: 当async被用作根协程时不会自动抛出异常外,但是如果async是其他协程的子协程,async中产生了一个异常,这个异常将就会被立即抛出。
这个异常即使try catch了也捕捉不到,原因是async (包含一个Job在它的CoroutineContext中) 会自动传播异常到它的父级 (launch),这会让异常被立即抛出,最终崩溃。
val coroutineScope2:CoroutineScope = CoroutineScope(context = Dispatchers.Main)
coroutineScope2.launch {
//todo 注意:这里很重要, 根协程CoroutineScope的直接子协程是launch()【不是async()】, launch()的子协程是async(),
// 协程语法规定: 当async被用作根协程时不会自动抛出异常外,但是如果async是其他协程的子协程,async中产生了一个异常,这个异常将就会被立即抛出。
// 这个异常即使try catch了也捕捉不到,原因是async (包含一个Job在它的CoroutineContext中) 会自动传播异常到它的父级 (launch),这会让异常被立即抛出,最终崩溃。
try {
val deferred: Deferred = [email protected]{
println("模拟抛出一个空指针异常")
throw NullPointerException()
return@async
}
val result:Unit = deferred.await()
} catch (e: Exception) {
// async 中抛出的异常将不会在这里被捕获
// 但是异常会被传播和传递到 scope
println("这里catch不会捕捉异常,异常向上传播,最终崩溃")
}
}
/**
* 程序直接崩溃 报异常
* */
协程的异常是会分发传播的,牵连到其他兄弟协程以及父协程。
当协程因出现异常失败时,它会将异常传播到它的父级,父级会取消其余的子协程,同时取消自身的执行。最后将异常再传播给它的父级。当异常到达当前层次结构的根,在当前协程作用域启动的所有协程都将被取消。
一般情况下这样的异常传播是合理的,但是在应用中处理与用户的交互,当其中一个子协程出现异常,那就可能导致所在作用域被取消,也就无法开启新的协程,最后整个UI组件都无法响应。
如果一个子协程发生了异常,那么其他的兄弟子协程就会被取消,父协程也会被取消 为了解决这个问题,官方给出的解决方案就是使用SupervisorJob
coroutineScope |
❶ 如果取消父协程或者父协程抛出异常,那么所有子协程都会被取消。 ❷如果子协程被取消,则不会影响同级其他子协程和父协程。 ❸如果子协程抛出异常,则会导致同级其他子协程被取消,将异常传递给父协程,父协程被取消,进而导致整个协程作用域失败。 比如一个网络请求失败了,所有其他的请求都将被立即取消,这种需求选择 |
|
❶ 它的取消操作只会向下传播, ❷一个子协程的运行异常失败,不会影响到其他子协程取消,内部的异常不会向上传播,不会影响父协程取消和兄弟协程的运行。 如果即使一个请求失败了其他的请求也要继续,则可以使用 |
SupervisorJob |
独立户 | ❶ ❷ ❸ SupervisorJob的一个子协程的运行失败或取消不会导致自己失败,也不会影响到其他子协程。 |
supervisorScope |
作用域 | 子协程抛出的异常并没有影响父级作用域以及作用域内的其他子协程的执行 |
coroutineScope的作用域使用案例
try {
coroutineScope {
println("1")
//第一个子协程job1 抛出异常NullPointerException 导致其他的子协程job2也会抛出取消异常CancellationException,而取消不会运行
// 同时异常还会传递给父协程 导致父协程发生异常 而取消不会运行
val job1:Job = launch {
println("2")
throw NullPointerException()//抛出空指针异常
}
//第二个子协程 由于第一个子协程异常 导致第二个子协程 取消
val job2 = launch {
delay(1000)
println("3")
}
// 使用 try…catch捕获 第二个子协程job2 抛出的取消异常CancellationException
try {
job2.join()
println("4")//等待第二个子协程完成:
} catch (e: Exception) {
println("5.输出捕获第二个协程job2 的取消异常: ${e}")//捕获第二个协程job2 的取消异常 :kotlinx.coroutines.JobCancellationException:
}
}
} catch (e: Exception) {//异常传递给父协程,父协程捕获异常
println("6.异常传递给父协程,父协程捕获异常: $e") //异常传递给父协程,父协程捕获异常: java.lang.NullPointerException
}
Thread.sleep(3000)//阻塞主线程3秒,以保持JVM存活,等待上面执行完成
/**
1
2
5.输出捕获第二个协程job2 的取消异常: kotlinx.coroutines.JobCancellationException: ScopeCoroutine is cancelling; job=ScopeCoroutine{Cancelling}@1ad282e0
6.异常传递给父协程,父协程捕获异常: java.lang.NullPointerException
*
*
* */
supervisorScope的作用域使用案例
try {
// job1抛出的异常并没有影响父级作用域以及作用域内的其他子协程job2的执行
supervisorScope {
println("1")
val job1:Job = launch {
println("2")
throw NullPointerException()// 子协程job1抛出空指针异常
}
// 子协程job2不受影响 继续执行
val job2 = launch {
delay(1000)
println("3")
}
try {
job2.join()
println("4")//等待第二个子协程完成:
} catch (e: Exception) {
println("5.输出捕获第二个协程job2 的取消异常: ${e}")
}
}
} catch (e: Exception) {
println("6.异常传递给父协程,父协程捕获异常: $e")
}
Thread.sleep(3000)//阻塞主线程3秒,以保持JVM存活,等待上面执行完成
/**
* job1抛出的异常并没有影响父级作用域以及作用域内的其他子协程job2的执行
* 1
2
* 3
4
*
* */
CoroutineExceptionHandler
异常处理器属于协程上下文的一种,需要将其添加到协程上下文中。 可以处理未捕获的异常。
异常如果需要被捕获,则需要满足下面两个条件:
launch
,而不是async
);CoroutineExceptionHandler
设置在CoroutineScope
的上下文中或者在一个根协程 (CoroutineScope
或者supervisorScope
的直接子协程) 中。
CoroutineScope的子协程上下文context设置异常处理器无效无意义 |
CoroutineScope的子协程上下文context设置异常处理器coroutineExceptionHandler没有意义无效,不会打印数据,因为异常向上传递给父协程,传递给父协程的异常处理器coroutineExceptionHandler去处理异常 所以父协程的上下文必须设置 异常处理器coroutineExceptionHandler 处理来自子协程的异常信息 否则子协程的传递来的异常无法捕获处理 |
SupervisorJob直接子协程的上下文context可以设置异常处理器 |
SupervisorJob直接子协程的上下文context可以设置异常处理器coroutineExceptionHandler 处理子协程自己内部的异常,因为SupervisorJob不会让异常向上传递 |
val coroutineExceptionHandler = CoroutineExceptionHandler { context, exception ->
println("捕获到的异常: $exception") // 捕获到的异常: java.lang.NullPointerException
}
runBlocking {
val coroutineScope = CoroutineScope(context=Job())
//上下文组合 context = Dispatchers.IO+coroutineExceptionHandler
//父协程中设置异常处理器coroutineExceptionHandler 是有效的 处理来自子协程的异常信息
val job = coroutineScope.launch(context = Dispatchers.IO + coroutineExceptionHandler, block = {
//子协程launchJob抛出空指针异常
//CoroutineScope的子协程上下文context设置异常处理器coroutineExceptionHandler没有意义无效,不会打印数据,因为异常向上传递给父协程,传递给父协程的异常处理器coroutineExceptionHandler去处理异常
//所以父协程的上下文必须设置 异常处理器coroutineExceptionHandler 处理来自子协程的异常信息 否则子协程的传递来的异常无法捕获处理
// val launchJob:Job = launch(context=Dispatchers.IO + coroutineExceptionHandler, block = {
val launchJob:Job = launch(context=Dispatchers.IO , block = {
throw NullPointerException()
})
//没有任何效果,执行await()会异常崩溃
val asyncDeferred:Deferred = async {
throw IllegalArgumentException()
return@async
}
val result:Unit= asyncDeferred.await() //这里会崩溃
})
job.join() //暂停协程,直到任务完成
println("============================================")
supervisorScope {
//todo SupervisorJob直接子协程的上下文context可以设置异常处理器coroutineExceptionHandler 处理子协程自己内部的异常,因为SupervisorJob不会让异常向上传递
val launchJob:Job = launch(context=Dispatchers.IO + coroutineExceptionHandler, block = {
throw ArithmeticException()//抛出算术运算异常
})
}
}
/**
* 捕获到的异常: java.lang.NullPointerException
* 捕获到的异常: java.lang.ArithmeticException
* */
注意:
1.协程内部使用
CancellationException
来进行取消,这个异常会被所有的处理者忽略,所以那些可以被catch
代码块捕获的异常仅仅应该被用来作为额外调试使用的资源。2.当协程的多个子协程因异常失败时,一般规则是"取第一个异常",因此将处理第一个异常。在第一个异常之后发生的所有其他异常都作为被抑制的异常绑定至第一个异常。
Channel
是非阻塞的通信基础设施,它实际上就是一个队列,而且是并发安全的,可以用来连接协程,实现不同协程的通信。
可以在两个或多个协程之间完成消息传递,多个作用域可以通过一个Channel
对象来进行数据的发送和接收。类似于BlockingQueue
+挂起函数,称为热数据流。
Channel
的可以说为协程注入了灵魂。每一个独立的协程不再是孤独的个体, Channel
可以让他们更加方便的协作起来。
创建Channel
的方式有两种:
Channel
对象创建,如上produce
:启动一个生产者协程,返回一个ReceiveChannel
。它启动的协程在结束后会自动关闭对应的Channel
。channel.send()
:发送数据。channel.close()
:关闭Channel,数据发送完毕。channel.receive()
:接收数据。
MainScope().launch {
// 1. 创建 Channel
val channel = Channel()
// 2. Channel 发送数据
launch(context=Dispatchers.IO,block={
for (i in 1..3) {
delay(100)
channel.send(i)//Channel 发送数据
}
channel.close()//关闭Channel,发送结束
})
// 3. channel.receive():接收数据。
// 一般调用 Channel#receive() 获取数据,但是这个方法只能获取一次传递的数据,如果我们必须知道获取数据的次数:
launch(context = Dispatchers.IO,block={
//重复3次接收数据
repeat(times = 3,action={
val receive = channel.receive()//接收数据
println("接收数据 $receive")
})
})
}
/**
* 接收数据 1
* 接收数据 2
* 接收数据 3
*
* */
MainScope().launch {
// 1. produce创建一个 Channel
// 拓展函数produce:启动一个生产者协程,返回一个ReceiveChannel。它启动的协程在结束后会自动关闭对应的Channel。
// 拓展函数produce直接将创建Channel和发送数据合为一步了。
val channel : ReceiveChannel = produce(context = Dispatchers.IO, block = {
for (i in 1..3) {
delay(100)
this.send(i)//发送数据
}
})
// 2.在另一个协程里接受数据, 接收数据
launch(context = Dispatchers.IO,block={
for (value in channel) {//for 循环打印接收到的值(直到渠道关闭)
println("接收数据: $value")
}
})
}
/**
* 接收数据: 1
* 接收数据: 2
* 接收数据: 3
* */
所谓冷数据流,就是只有消费时才会生产的数据流。
Flow
是一种异步数据流,它按顺序发出值并正常或异常完成。是 Kotlin 协程的响应式API,类似于 RxJava 的存在。Flow
不会阻塞主线程的运行
Flow
是一种冷数据流,流生成器中的代码直到流被收集起来才会运行。一个Flow
创建出来之后,不消费则不生产,多次消费则多次生产,生产和消费总是相对应的。
Channel 热数据流 |
无观察者时,也会生产数据。你不订阅,它也会发送数据。 比如某场影片在电影院播放,你要去电影院看才能看到,你不去这场电影也是会正常放的; |
Flow冷数据流 | 无消费者时,则不会生产数据。你触发了,它才有数据发送过来。 比如这场电影在网络上公开了,你不去播放他就不会播放,你主动播放了他才会播放。RxJava相对应的是协程的冷数据流 |
Flow : |
创建Flow 的普通方法,从给定的一个挂起函数创建一个冷数据流。线程不安全 |
channelFlow : |
支持缓冲通道,线程安全,允许不同的CorotineContext 发送事件。 |
T.asFlow() : |
将其他数据转换成普通的
|
flowof(vararg elements: T) : |
使用可变数组快速创建
|
数据源创建Flow |
|
flow(block: suspend FlowCollector |
collect 订阅接收消费数据 |
接收收集器 |
public interface Flow } |
发送 数据 |
收集上游的数据并发出。不是线程安全,不应该并发调用。线程安全请使用 |
public fun interface FlowCollector |
lifecycleScope.launch(context = Dispatchers.IO,block={
//1.创建一个Flow
val flow: Flow = flow {
for (i in 1..3) {
delay(200)
//2.从冷流中发出数据
this.emit(i)
}
}
//3.从flow冷流中获取数据
flow.collect(collector={
println("从flow冷流中获取数据:$it")
})
})
/**
*
* 从flow冷流中获取数据:1
* 从flow冷流中获取数据:2
* 从flow冷流中获取数据:3
*
* */
改变发送数据emit的线程 |
Flow |
改变消费数据collect的线程 | 它自动切回所在协程的调度器 |
注意:不允许在内部使用
withContext()
来切换flow
的线程。因为flow
不是线程安全的,如果一定要这么做,请使用channelFlow
。
Flow
的异常处理也比较直接,直接调用 catch
函数捕获异常即可。
在Flow
的参数中抛了一个空指针异常,在catch
函数中就可以直接捕获到这个异常。如果没有调用catch
函数,未捕获异常会在消费时抛出。catch
函数只能捕获它的上游的异常。
注意:流收集还可以使用
try{}catch{}
块来捕获异常。
lifecycleScope.launch {
flow {
this.emit(10) //从流中发出值
throw NullPointerException()//抛出空指针异常
}.catch(action={ println("flow捕获上游的异常 caught error: $it") })
.collect{ println("收集获取数据:$it") }
}
/**
* 收集获取数据:10
* flow捕获上游的异常 caught error: java.lang.NullPointerException
* */
如果我们想要在流完成时执行逻辑,可以使用 onCompletion
函数:
注意:流还可以使用
try{}finally{}
块在收集完成时执行一个动作。
lifecycleScope.launch {
flow{
this.emit(10)
}.onCompletion(action={ println("执行Flow 操作完成回调") }) //流操作完成回调
.collect{ println("收集获取数据:$it") }
}
/**
* 收集获取数据:10
* 执行Flow 操作完成回调
* */
Flow
没有提供取消操作,Flow
的取消依赖于collect
末端操作符,而它们又必须在协程当中调用,因此Flow
的取消主要依赖于末端操作符所在的协程的状态。
想要取消Flow
只需要取消它所在的协程。
//创建一个作用域
lifecycleScope.launch {
//1.创建一个子协程
val job = launch {
//2.创建flow
val intFlow = flow {
(1..6).forEach {
delay(1000)
//3.发送数据
this.emit(it)
}
}
//4.收集数据
intFlow.collect{
println("输出收集获取数据:$it")
}
}
//5.在3.5秒后取消协程job
delay(3500)
job.cancelAndJoin()
}
/**
* 输出收集获取数据:1
* 输出收集获取数据:2
* 输出收集获取数据:3
* //输出收集获取数据:4 //取消了flow所在的协程 不在输出
* //输出收集获取数据:5 //取消了flow所在的协程 不在输出
* //输出收集获取数据:6 //取消了flow所在的协程 不在输出
*
* */
什么是背压?就是在生产者的生产速率高于消费者的处理速率的情况下出现,发射的数据量大于消费的数据量,造成了阻塞,就相当于压力往回走,这就是背压。只要是响应式编程,就一定会有背压问题。
处理背压问题有以下三种方式:
处理背压问题的方式 | 说明 |
|
指定固定容量的缓存; 为 |
conflate 保留最新值 |
|
新值发送时,取消之前的 |
Flow 基本操作符 | 作用 |
---|---|
map |
转换操作符,将值转换为另一种形式输出 |
take |
接收指定个数发出的值 |
filter |
过滤操作符,返回只包含与给定规则匹配的原始值的流。 |
flow末端流操作符 | 作用 |
---|---|
collect |
最基础的收集数据,触发flow的运行 |
toCollection |
将结果添加到集合 |
launchIn |
在指定作用域直接触发流的执行 |
toList |
给定的流收集到 List 集合 |
toSet |
给定的流收集到 Set 集合 |
reduce |
规约,从第一个元素开始累加值,并将参数应用到当前累加器的值和每个元素。 |
fold |
规约,从[初始]值开始累加值,并应用[操作]当前累加器值和每个元素 |
功能性操作符 | 作用 |
---|---|
retry |
重试机制 ,当流发生异常时可以重新执行 网络请求使用 |
cancellable |
接收数据的时候判断 协程是否被取消 ,如果已取消,则抛出异常 |
debounce |
防抖节流 ,指定时间内的值只接收最新的一个,其他的过滤掉。 |
回调流操作符 | 作用 |
---|---|
onStart |
在上游流开始之前被调用。 可以发出额外元素,也可以处理其他事情,比如发埋点 |
onEach |
在上游向下游发出元素之前调用 |
onEmpty |
当流完成却没有发出任何元素时回调,可以用来兜底。 |
组合流操作符 | 作用 |
---|---|
zip |
组合两个流,分别从二者取值,一旦一个流结束了,那整个过程就结束了 |
combine |
组合两个流,在经过第一次发射以后,任意方有新数据来的时候就可以发射,另一方有可能是已经发射过的数据 数据合并使用 |
展平流有点类似于 RxJava 中的 flatmap
,将你发射出去的数据源转变为另一种数据源。
展平流操作符 | 作用 |
---|---|
flatMapConcat |
串行处理数据,展开合并成一个流 |
flatMapMerge |
并发地收集所有流,并将它们的值合并到单个流中,以便尽快发出值 |
flatMapLatest |
一旦发出新流,就取消前一个流的集合 |