目录
- 1、什么是 Kotlin 协程
- 2、场景举例
- 3、如何使用 Kotlin 协程
- 4、实现第一个协程
- 5、CoroutineScope(作用域)
- 5.1、GlobalScope
- 5.2、runBlocking
- 5.3、coroutineScope
- 5.4、supervisorScope
- 6、suspend(挂起)
- 7、CoroutineContext(协程上下文)
- 7.1、Dispatchers(协程调度器)
- 7.2、CoroutineName(协程命名)
- 7.3、CoroutineExceptionHandler(协程异常捕捉)
- 7.4、组合上下文
- 8、CoroutineBuilder(协程构建器)
- 8.1、launch
- 8.2、async
- 9、参考
1、什么是 Kotlin 协程
本质上是一个轻量级的线程,可以很方便实现线程间切换,并且支持非阻塞式的挂起。
2、场景举例
我们需要同时请求多个接口,并且把返回值组装起来,按照 callback
方式伪代码如下。
HttpRequest(context).url("/xxx/xxx").callback(object : HttpCallback<String>() {
override fun onResponse(response: String?) {
// 第一个接口请求成功,发起第二个请求
HttpRequest(context).url("/xxx/xxx").callback(object : HttpCallback<String>() {
override fun onResponse(response: String?) {
// 第二个接口请求成功
// do something
}
}).get()
}
}).get()
使用 callback 的方式,使得本可以同时请求的接口,不得不变成的顺序执行,从流程上接口调用时间增加了一倍,且可读性很差。
3、如何使用 Kotlin 协程
由于协程不在 Kotlin 基础库中,所以需要添加依赖:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7"
4、实现第一个协程
fun main() {
log("start")
GlobalScope.launch(context = Dispatchers.IO) {
// 延时 1000 ms
delay(1000)
// log 打印当前线程的名称
log("launch")
}
// 休眠 2000 ms
Thread.sleep(2000)
log("end")
}
xxx 16:13:29.045 2006-2006/xxx D/qqq: [main] start
xxx 16:13:30.098 2006-3050/xxx D/qqq: [DefaultDispatcher-worker-1] launch
xxx 16:13:31.092 2006-2006/xxx D/qqq: [main] end
例子中,通过 GlobalScope
启动了一个协程,在延迟一秒后输出一行日志。从输出结果可以看出,启动的协程是运行在协程内部的线程池中。虽然从表现结果来看,启动一个协程类似于我们直接使用 Thread 来执行耗时任务,但实际上协程和线程有着本质上的区别。通过使用协程,可以极大的提高线程的并发效率,避免以往嵌套的回调地狱,极大的提高了代码的可读性。
以上代码涉及了协程的四个基本概念:
-
CoroutineScope
,即协程作用域,GlobalScope 是 CoutineScope 的一个实现类,用于指定协程的作用范围,可用于管理多个协程的生命周期。 -
suspend function
,即挂起函数,这里的delay()
就是协程库提供的一个非阻塞式延时的挂起函数。 -
CoroutineContext
,即协程上下文,Dispatcher.IO 就是其中一种配置参数,用于指定协程运行在哪一类线程上。 -
CoroutineBuilder
,即线程构建器,通过 launch、async 等协程构建器来进行声明并启动。launch、async 均为 CoroutineScope 的扩展函数。
5、CoroutineScope(作用域)
CoroutineScope
即协程作用域,用于指定协程的作用范围,并可以进行统一管理。所有的协程都需要通过 CoroutineScope 来启动,它会跟踪创建的所有协程,可以调用 scope.cancel()
取消正在运行的协程。在 Android 中,某些 ktx 库为某些生命周期类提供了自己的 CoroutineScope,例如 ViewModel 的 viewModelScope
,Lifecycle 的 lifecycleScope
。
CoroutineScope 大体上可以分为三种:
-
GlobalScope
,即全局协程作用域,内部协程可以一直运行到应用停止运行,不会阻塞当前线程,且启动的协程相当于守护线程,不会阻止 JVM 结束运行。 -
runBlocking
,一个顶层函数,和 GlobalScope 不一样,它会阻塞当前线程,直到其内部所有相同作用域的协程执行结束。 -
自定义 CoroutineScope
,可用于实现主动控制协程的生命周期范围,对于 Android 开发来说最大意义之一就是可以在 Activity、Fragment、ViewModel 等具有生命周期的对象中按需取消所有协程任务,从而确保生命周期安全,避免内存泄漏。
5.1、GlobalScope
GlobalScope 是全局作用域,通过它启动的协程的生命周期,只会受整个应用程序的生命周期限制。只要应用程序还在运行,且协程任务还没有结束,就可以一直运行。
fun startGlobalScope() {
log("start")
// GlobalScope 是 CoroutineScope 的实现类
GlobalScope.launch {
launch {
// delay 是非阻塞的,有 suspend 修饰符
delay(400)
log("launch A")
}
launch {
delay(300)
log("launch B")
}
log("GlobalScope")
}
log("end")
Thread.sleep(500)
}
xxx 17:31:08.023 7142-7142/xxx D/qqq: [main] start
xxx 17:31:08.058 7142-7142/xxx D/qqq: [main] end
xxx 17:31:08.062 7142-7324/xxx D/qqq: [DefaultDispatcher-worker-1] GlobalScope
xxx 17:31:08.366 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] launch B
xxx 17:31:08.466 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] launch A
根据日志可以看出 GlobalScope
不会阻塞当前线程。
5.2、runBlocking
runBlocking 函数的第二个参数被声明为 CoroutineScope 的扩展函数,所以在其内部就可以直接启动协程。
public fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T {
// ...
}
runBlocking 示例:
fun startRunBlocking() {
log("start")
runBlocking {
launch {
repeat(3) {
delay(100)
log("A $it")
}
}
launch {
repeat(3) {
delay(100)
log("B $it")
}
}
GlobalScope.launch {
repeat(3) {
delay(120)
log("GlobalScope $it")
}
}
}
log("end")
}
xxx 17:36:58.256 7142-7142/xxx D/qqq: [main] start
xxx 17:36:58.365 7142-7142/xxx D/qqq: [main] A 0
xxx 17:36:58.366 7142-7142/xxx D/qqq: [main] B 0
xxx 17:36:58.385 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] GlobalScope 0
xxx 17:36:58.467 7142-7142/xxx D/qqq: [main] A 1
xxx 17:36:58.468 7142-7142/xxx D/qqq: [main] B 1
xxx 17:36:58.507 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] GlobalScope 1
xxx 17:36:58.569 7142-7142/xxx D/qqq: [main] A 2
xxx 17:36:58.570 7142-7142/xxx D/qqq: [main] B 2
xxx 17:36:58.571 7142-7142/xxx D/qqq: [main] end
xxx 17:36:58.628 7142-7326/xxx D/qqq: [DefaultDispatcher-worker-2] GlobalScope 2
根据日志可以看出,runBlocking 会阻塞当前线程,但其内部是非阻塞的,当内部相同作用域的所有协程都运行结束后,才会执行 runBlocking 后面的代码。
5.3、coroutineScope
用于创建一个独立的协程作用域,直到开启的协程任务全部完成后才结束自身,和 runBlocking 的区别在于,coroutineScope 不阻塞线程,且它是一个挂起函数。
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
...
}
5.4、supervisorScope
用于创建一个使用了 SupervisorJob 的 coroutineScope,该作用域的特点是抛出的异常不会连锁取消同级协程和父协程。
/**
* Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* context's [Job] with [SupervisorJob].
*
* A failure of a child does not cause this scope to fail and does not affect its other children,
* so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for details.
* A failure of the scope itself (exception thrown in the [block] or cancellation) fails the scope with all its children,
* but does not cancel parent job.
*/
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
...
}
6、suspend(挂起)
如果把之前例子中的 delay() 函数移动到 GlobalScope 外面调用的话,会发现代码错误:Suspend function 'delay' should be called only from a coroutine or another suspend function。意为 delay() 函数是一个挂起函数,只能由协程或者由其他挂起函数来调用,看看 delay() 的源码可见,函数前多了 suspend 修饰符。
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
聊到协程的非阻塞特性,往往都离不开 suspend 这个概念,究竟是怎么实现非阻塞的呢,这里涉及到两个概念:挂起和恢复。
在我们使用 Thread.sleep() 的时候,代码执行到这一行,就休眠不在继续执行了,会等待休眠结束后继续执行。
线程阻塞.gif如果使用协程做呢,可以发现日志是交替执行的,并没有发生阻塞。
协程中存在一个类似调度中心的东西,在协程执行时,调度中心会将协程挂起,不阻碍后续的任务执行,在特定的时候,再回来继续执行。提高了利用率。
协程挂起.gif7、CoroutineContext(协程上下文)
协程上下文常用的几个实现类:Dispatchers
、CoroutineName
、CoroutineExceptionHandler
7.1、Dispatchers(协程调度器)
Dispatchers 用于指定协程的目标载体,即运行在哪个线程上。Kotlin 提供了四个 Dispatcher:
-
Dispatchers.Default
默认调度器,适合用于执行占用大量 CPU 资源的任务。例如:对列表排序和解析 JSON。 -
Dispatchers.IO
适合用于执行磁盘或网络 I/O 的任务。例如:使用 Room 组件、读写磁盘文件,执行网络请求。 -
Dispatchers.Unconfined
对执行协程的线程不做限制,可以直接在当前调度器所在线程上执行。 -
Dispatchers.Main
使用此调度程序可用于在 Android 主线程上运行协程,只能用于与界面交互和执行快速工作,例如:更新 UI、调用 LiveData.setValue。
7.2、CoroutineName(协程命名)
如果一个协程中有多个子协程,如果你想知道谁是谁,就可以通过 CoroutineName 来命名,使用如下:
fun main(){
val coroutineName = CoroutineName("MyCoroutine")
GlobalScope.launch(coroutineName) {
log("start ${coroutineName.name}")
}
}
7.3、CoroutineExceptionHandler(协程异常捕捉)
可以通过传入一个异常捕捉的上下文,将协程中出现的异常统一抛出来进行处理。
fun main() {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
myPrint("exception: ${exception.message}")
}
GlobalScope.launch(exceptionHandler) {
throw RuntimeException("test exception")
}
}
7.4、组合上下文
上面介绍了协程常用的一些上下文实现类,如果我想同时拥有怎么办?协程提供了+
运算符来组合上下文。
fun main() {
val coroutineName = CoroutineName("MyCoroutine")
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
myPrint("exception: ${exception.message}")
}
GlobalScope.launch(coroutineName + exceptionHandler) {
throw RuntimeException("test exception")
}
}
8、CoroutineBuilder(协程构建器)
8.1、launch
以下为launch
数的源代码,它是一个作用于 CoroutineScope 的扩展函数,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程的引用 Job 对象。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
launch
数共包含三个参数:
- context 用于指定协程的上下文。
- start 用于指定协程的启动方式。
- block。用于传递协程的执行体。
8.2、async
async
也是一个作用于 CoroutineScope 的扩展函数,和 launch
的区别主要就在于,async
可以返回协程的执行结果,而 launch
不行,可以看到 async
返回的是一个 Deferred
对象。
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
通过 await()
方法可以拿到 async
协程的执行结果。
网友评论