协程是轻量级的线程。
kotlin协程是kotlin的扩展库(kotlinx.coroutines)。
线程在Android开发中一般用来做一些复杂耗时的操作,避免耗时操作阻塞主线程而出现ANR的情况,例如IO操作就需要在新的线程中去完成。如果一个页面中使用的线程太多,线程间的切换是很消耗内存资源的,我们都知道线程是由系统去控制调度的,所以线程使用起来比较难于控制。kotlin协程是运行在线程之上的,它的切换由程序自己来控制,无论是 CPU 的消耗还是内存的消耗都大大降低
它们在某些 CoroutineScope 上下文中与 launch协程构建器 一起启动。
启动协程的三种方式
1. runBlocking:T
2. launch:Job
3. async/await:Deferred
启动模式
viewModelScope.launch(start = CoroutineStart.LAZY) {}
start 指定启动模式
在 Kotlin 协程当中,启动模式是一个枚举:四个启动模式当中我们最常用的其实是 DEFAULT 和 LAZY
CoroutineStart. LAZY只有在需要的情况下运行
LAZY 是懒汉式启动,launch 后并不会有任何调度行为,协程体也自然不会进入执行状态,直到我们需要它执行的时候
调用 Job.start,主动触发协程的调度执行
调用 Job.join,隐式的触发协程的调度执行
CoroutineStart. DEFAULT立即执行协程体
launch 调用后,会立即进入待调度状态,一旦调度器 OK 就可以开始执行
CoroutineStart. ATOMIC立即执行协程体,但在开始运行之前无法取消.因此ATOMIC 模式,因此协程一定会被调度
CoroutineStart. UNDISPATCHED立即在当前线程执行协程体,直到第一个 suspend 调用
在GlobalScope 中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。
我们可以在执行操作 所在的 指定作用域内启动协程, 而不是像通常使用线程(线程总是全局的)那样在 GlobalScope 中启动。
包括 runBlocking 在内的每个协程构建器都将 CoroutineScope 的实例添加到其代码块所在的作用域中。
除了由不同的构建器提供协程作用域之外,还可以使用 coroutineScope 构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。
将launch { ...... 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有suspend修饰符的新函数。
在协程内部可以像普通函数一样使用挂起函数, 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 delay****)来挂起协程的执行。
launch 函数返回了一个 可以 被用来 取消运行中 的协程的 Job
job.cancel() 取消该作业
job.join() ****等待作业执行结束
一旦 main 函数调用了 job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了。
这里也有一个可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。
协程的取消是 协作 的。
一段协程代码必须协作才能被取消。
所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。
它们检查协程的取消, 并在取消时抛出 CancellationException。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的
。isActive 是一个可以被使用在 CoroutineScope 中的扩展属性。
有两种方法来使执行计算的代码可以被取消。
第一种方法是定期调用挂起函数来检查取消。对于这种目的 yield是一个好的选择。
另一种方法是显式的检查取消状态(isActive)
概念上,async 就类似于 launch。
它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。
不同之处在于
launch返回一个 Job 并且不附带任何结果值,
async返回一个 Deferred---一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。
你可以使用 .await()在一个延期的值上得到它的最终结果,
但是 Deferred也是一个 Job,所以如果需要的话,你可以取消它。
async 可以通过将 start参数设置为 CoroutineStart.LAZY 而变为惰性的。 在这个模式下,只有结果通过await 获取的时候协程才会启动,或者在 Job 的 start 函数调用的时候
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }// 执行一些计算
one.start() // 启动第一个
two.start() // 启动第二个
println("The answer is ${one.await() + two.await()}")
请注意,如果其中一个子协程(即 two)失败,第一个 async以及等待中的父协程都会被取消
协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行
所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。
当调用 launch { ...... } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)
当协程在 GlobalScope 中启动时,使用的是由 Dispatchers.Default 代表的默认调度器。
默认调度器使用共享的后台线程池。
所以 launch(Dispatchers.Default) { ...... }与 GlobalScope.launch { ...... }使用相同的调度器
使用 runBlocking 来显式指定了一个上下文,并且另一个使用 withContext 函数来改变协程的上下文,而仍然驻留在相同的协程中
当一个协程 被 其它协程 在 CoroutineScope 中启动的时候,
它将通过 CoroutineScope.coroutineContext 来承袭上下文,
并且这个新协程的 Job 将会成为父协程作业的 子 作业。
当一个父协程被取消的时候,所有它的子协程也会被递归的取消。
我们通过创建一个 CoroutineScope 实例来管理协程的生命周期,并使它与 activity 的生命周期相关联。CoroutineScope****可以通过 CoroutineScope() 创建或者通过MainScope() 工厂函数。前者创建了一个通用作用域,而后者为使用 Dispatchers.Main 作为默认调度器的 UI 应用程序
classActivity{
private val mainScope = MainScope()
fun destroy() { mainScope.cancel() }
如果使用一些消耗 CPU 资源的阻塞代码计算数字(每次计算需要 100 毫秒)那么我们可以使用 Sequence 来表示数字
suspend fun simple(): List<Int> {
delay(1000) // pretend we are doing something asynchronous here
return listOf(1, 2, 3)
}
fun main() = runBlocking<Unit> {
simple().forEach { value -> println(value) }
}
使用 List 结果类型,意味着我们只能一次返回所有值。
为了表示异步计算的值流(stream),我们可以使用 Flow 类型(正如同步计算值会使用 Sequence 类型)
fun simple(): **Flow<Int>** = **flow { // flow builder**
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
**emit(i) // emit next value**
}
}
fun main() = runBlocking<Unit> {
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
**simple().collect { value -> println(value) }**
}}
注意使用 Flow 的代码与先前示例的下述区别:名为 flow 的 Flow 类型构建器函数。flow { ... }构建块中的代码可以挂起。所以函数 simple 不再标有 suspend 修饰符。
流使用 emit 函数 发射 值
流使用 collect 函数 收集 值
启动协程需要三样东西,分别是 上下文****、启动模式、协程体,
协程体 就好比 Thread.run
当中的代码
启动协程 三种方式:
Launch 返回Job
async 返回deferred,延迟稍后才能拿到 await获取
runblocking(仅仅用于单元测试)
调度器(上下文 CoroutineContext)
调度器的目的就是切线程
本身是协程上下文的子类,同时实现了拦截器的接口, dispatch 方法会在拦截器的方法 interceptContinuation 中调用,进而实现协程的调度
现成的,它们定义在 Dispatchers 当中
Dispatchers.main UI 线程
Dispatchers.io 它基于 Default 调度器背后的线程池,并实现了独立的队列和限制,因此协程调度器从 Default 切换到 IO 并不会触发线程切换。
Dispatchers.default 线程池
Dispatchers.unconfined 直接执行
协程拦截器是一个上下文的实现方向,拦截器可以左右你的协程的执行,同时为了保证它的功能的正确性,协程上下文集合永远将它放在最后面
它拦截协程的方法也很简单,因为协程的本质就是回调 + “黑魔法”,而这个回调就是被拦截的 Continuation 了。调度器就是基于拦截器实现的,换句话说调度器就是拦截器的一种。
如果我们在拦截器当中自己处理了线程切换,那么就实现了自己的一个简单的调度器
自定义一个拦截器
class MyContinuationInterceptor: ContinuationInterceptor{
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}
class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
override val context = continuation.context
override fun resumeWith(result: Result<T>) {
log("<MyContinuation> $result" )//打印日志
continuation.resumeWith(result)
}
}
调用自定义拦截器 放到我们的协程上下文
GlobalScope.launch(MyContinuationInterceptor()) {}
withContext****这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。
**withContext**
是一个 **suspend**
函数,它需要在协程或者是另一个 **suspend**
函数中调用
通过 withContext
源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher
参数,依赖这个 Dispatcher
参数的指示,你的协程被挂起,然后切到别的线程。
**suspend**
是 Kotlin 协程最核心的关键字,几乎所有介绍 Kotlin 协程的文章和演讲都会提到它。它的中文意思是「暂停」或者「可挂起」
紧接着在 suspend
函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。
我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers
切换到了 IO 线程;
当这个函数执行完毕后,线程又切了回来,「切回来」也就是协程会 post
一个 Runnable
,让剩下的代码继续回到主线程去执行。
协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;
不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程。
再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作。
这个「切回来」的动作,在 Kotlin 里叫做 resume,恢复。
suspend****关键字只起到了标志这个函数是一个耗时操作,必须放在协程中执行的作用,而withContext方法则进行了线程的切换工作
什么时候需要自定义 suspend 函数
如果你的某个函数比较耗时,也就是要等的操作,那就把它写成 suspend 函数。这就是原则。
耗时操作一般分为两类:I/O 操作和 CPU 计算工作。比如文件的读写、网络交互、图片的模糊处理,都是耗时的,通通可以把它们写进 suspend 函数里。
另外这个「耗时」还有一种特殊情况,就是这件事本身做起来并不慢,但它需要等待,比如 5 秒钟之后再做这个操作。这种也是 suspend 函数的应用场景。
具体该怎么写
给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了。
提到用 withContext是因为它在挂起函数里功能最简单直接:把线程自动切走和切回。
当然并不是只有 withContext 这一个函数来辅助我们实现自定义的 suspend 函数,比如还有一个挂起函数叫 delay,它的作用是等待一段时间后再继续往下执行代码。
用 launch 函数来创建协程,其实还有其他两个函数也可以用来创建协程:
- runBlocking
- async
runBlocking 通常适用于单元测试的场景,而业务开发中不会用到这个函数,因为它是线程阻塞的。
接下来我们主要来对比 launch 与 async 这两个函数。
· 相同点:它们都可以用来启动一个协程,返回的都是 Coroutine
,我们这里不需要纠结具体是返回哪个类。
· 不同点:**async**
返回的 Coroutine
多实现了 Deferred
接口。
关于 Deferred
它的意思就是延迟,也就是结果稍后才能拿到。
我们调用 Deferred.await()
就可以得到结果。
启动一个协程可以使用 **launch**
或者 **async**
函数,协程其实就是这两个函数中闭包的代码块
**Dispatchers**
调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行
常用的 Dispatchers ,有以下三种:
- Dispatchers.Main:Android 中的主线程
- Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
- Dispatchers.Default:适合 CPU 密集型的任务,比如计算
协程就是个线程框架,协程的挂起本质就是线程切出去再切回来
协程就是切线程; 挂起就是可以自动切回来的切线程;
挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,就这么简单
Job.join() 保证主线程 等待协程执行完毕
协程的取消 跟线程的取消差不多
Job.cancel
Job.cancelandjoin
那么协程自动进行线程切换的原理是什么?
Yield 让出CPU,放弃调度控制权,回到上一次Resume的地方
Resume 获取调度控制权,继续执行程序,到上一次Yield的地方
https://mp.weixin.qq.com/s/RgAC1Q1J6BxnrJF12WQQ1w
https://developer.android.google.cn/topic/libraries/architecture/coroutines#viewmodelscope
Kotlin 协程提供了一个可供您编写异步代码的 API。通过 Kotlin 协程,您可以定义 CoroutineScope
,以帮助您管理何时应运行协程。每个异步操作都在特定范围内运行。
架构组件针对应用中的逻辑范围以及与 LiveData
的互操作层为协程提供了一流的支持。
使用 **liveData**
构建器函数 调用 **suspend**
函数,并将结果作为 LiveData
对象传送
loadUser()
是在其他位置声明的暂停函数。使用 liveData
构建器函数异步调用 loadUser()
,然后使用**emit()**
发出结果
val user: LiveData<User> = liveData {
// loadUser is a suspend function.
val data = database.loadUser()
emit(data)
}