前言
Kotlin
是一种在Java
虚拟机上运行的静态类型编程语言,被称之为Android
世界的Swift
,在Google
I/O2017中,Google
宣布Kotlin
成为Android
官方开发语言
Kotlin协程介绍
在Java
中我们使用多线程Thread
来进行并发,在Flutter
框架的Dart
语言中我们使用Future
的基于事件驱动的异步模型以及Isolate
这种隔离的轻量级线程去实现并发操作,在Kotlin
开发的Android
项目中得益于一些优秀的框架比如Rxjava
等去进行一些并发的操作,这里主要学习下Kotlin
中的协程使用,以及与Java
中的线程,Dart
中的Isolate
进行一些比较
协程(coroutines
)是一种并发设计模式,您可以在Android
平台上使用它来简化异步执行的代码。协程是在版本 1.3 中添加到 Kotlin
的,它基于来自其他语言的既定概念
在 Android
上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。使用协程的专业开发者中有超过 50% 的人反映使用协程提高了工作效率
协程的特点
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发
协程中的元素
Coroutine scope
协程的作用域
Job
任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期
Coroutine context
协程上下文,协程上下文里是各种元素的集合
Coroutine dispatchers
协程调度,可以指定协程运行在 Android
的哪个线程里
suspend
挂起函数,挂起,就是一个稍后会被自动切回来的线程调度操作
在Android Gradle中引入Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
如何启动协程序
runBlocking
GlobalScope.launch
async,await
runBlocking
运行一个新的协程,会阻塞当前线程,直到代码块运行完毕,由于是代码阻塞的,不是用于协程,而是设计用于单元测试
* @param context the context of the coroutine. The default value is an event loop on the current thread.
* @param block the coroutine code.
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
* This function should not be used from a coroutine. It is designed to bridge regular blocking code
* to libraries that are written in suspending style, to be used in `main` functions and in tests.
public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
fun test() {
Log.e("Mike", "Test Start")
runBlocking {
Log.e("Mike", "runBlocking start ${Thread.currentThread().name}")
Thread.sleep(2000)
Log.e("Mike", "runBlocking end ${Thread.currentThread().name}")
}
Log.e("Mike", "Test End")
}
运行结果
Test Start
runBlocking start main
//两秒后
runBlocking end main
Test End
GlobalScope.launch
会启动一个新的协程,而不会阻塞当前线程,返回一个协程Job
,Job
可用来控制对应协程
序,比如cancel
参数一 context
CoroutineContext
协程的上下文对象,不是Android
的Context
,默认值为EmptyCoroutineContext
参数一start
CoroutineStart
协程的启动配置,默认值为CoroutineStart.DEFAULT
参数一 block
suspend CoroutineScope.() -> Unit
执行的代码块
返回值 Job
用于控制此携程的生命周期
* @param context additional to [CoroutineScope.coroutineContext] context of the coroutine.
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
* @param block the coroutine code which will be invoked in the context of the provided scope.
* Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].
* The coroutine is cancelled when the resulting job is [cancelled][Job.cancel].
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun test() {
//三种方式使用携程
//1.runBlocking 顶层函数
Log.e("Mike", "Test Start")
GlobalScope.launch {
//this is CoroutineScope
Log.e("Mike", "launch start ${Thread.currentThread().name}")
Thread.sleep(2000)
Log.e("Mike", "launch end ${Thread.currentThread().name}")
}
Log.e("Mike", "Test End")
}
运行结果
Test Start
Test End
launch start DefaultDispatcher-worker-1
//两秒后
launch end DefaultDispatcher-worker-1
async,await
之前在Dart
语言中在基于事件的异步模型中,await
用于等待耗时事件的返回,会阻塞函数后续执行,只能用在async
函数中,async
用于函数的后缀,表示此函数是一个异步函数,并有一个未来结果返回
Dart语言学习-异步编程async和await
Deferred
表示延期返回,对应Dart
语言中的Future
也就是未来返回
Dart语言学习-异步编程Future
在Kotlin协程中,它们拥有类似的功能
async
CoroutineScope的函数,返回值Deferred
,表示一个延期返回的结果
public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred
await
是Deferred
对象的函数,用于等待未来结果的返回,会阻塞当前线程直到返回结果
public suspend fun await(): T
所以我们的执行流程为
1.在CoroutineScope
对象中通过async
执行一个耗时操作,返回一个Deferred
对象
2.使用await
等待Deferred
的返回结果
使用async,await
实现这样的场景
在程序开始启动时打印启动时间,获取远端数据,初始化配置,远端数据完成之后在主线程将其渲染
fun init() {
printInitTime() //打印启动时间
GlobalScope.launch { //获取远端数据需要耗时,创建一个协程运行在子线程,不会阻塞
val deferred = async { //使用 async 执行一个耗时任务,返回一个deferred
Log.e("Mike", "getNetworkData start in ${Thread.currentThread().name}")
Thread.sleep(2000)
"response data"
}
val response = deferred.await() //等待deferred 的返回
Log.e("Mike", "getNetworkData end in${Thread.currentThread().name}")
GlobalScope.launch(Dispatchers.Main) { //启动一个协程,运行在主线程
Log.e("Mike", "display $response in ${Thread.currentThread().name}");
}
}
initConfig() //初始化配置
}
private fun printInitTime() {
Log.e("Mike", "current time is ${System.currentTimeMillis()}")
}
private fun initConfig() {
Log.e("Mike", "initConfig");
}
打印结果
current time is 1624871044577
initConfig
getNetworkData start in DefaultDispatcher-worker-3
//两秒后
getNetworkData end inDefaultDispatcher-worker-3
display response data in main
Dispatchers
上面我们使用到了Dispatchers.Main使协程运行到了主线程,不同的Dispatchers定义了不同的协程调度模式
Dispatchers.Main
Android主线程,也可以使用MainScope().launch { }
Dispatchers.Unconfined
沿用了当前CoroutineScope的线程策略
Dispatchers.Default
默认值的CoroutineDispatcher,JVM共享线程池,保证最大程度的并行性,线程数量等于CPU的核心数,最少为2
Dispatchers.IO
IO线程池,它默认为64个线程的极限或核的数量的最大值
CoroutineStart
表示协程的执行模式
CoroutineStart.DEFAULT
默认的协程执行模式,根据上下文立即执行
CoroutineStart.LAZY
懒惰模式,在后续需要使用到的时候才会根据上下文执行
CoroutineStart.ATOMIC
和默认的协程执行模式相同,会根据上下文立即执行,但是在协程开始之前无法对其进行cancel
CoroutineStart.UNDISPATCHED
非阻塞式挂起
协程有个特点就是支持非阻塞式挂起,阻塞的意思就是会阻断当前线程后面的代码不会执行,那么挂起是什么意思
dealy
与 Thread.sleep()
dealy
在之前的多线程开发中,我们使用Thread.sleep()
去进行线程的休眠延时,阻塞当前线程的执行,在协程中我们使用dealy
去进行延时操作,sleep
会进行阻塞调用休眠完成之后才会进行后续,dealy
则不会阻塞会导致当前协程非阻塞挂起,非阻塞意为不会阻塞其他协程,挂起意为当前协程会挂起等待
下面使用dealy
的案例中Coroutine 1
中的delay
不会阻塞Coroutine 2
协程的执行,只是会挂起自己,在执行完毕之后会停止挂起打印launch1 end
fun testHang(){
Log.e("Mike", "testHang start")
GlobalScope.launch {
//Coroutine 1
GlobalScope.launch(Dispatchers.Main) {
Log.e("Mike", "launch1 start")
delay(2000)
Log.e("Mike", "launch1 end")
}
Log.e("Mike", "testHang next")
//Coroutine 2
GlobalScope.launch(Dispatchers.Main) {
Log.e("Mike", "launch2 start")
delay(2000)
Log.e("Mike", "launch2 end")
}
}
Log.e("Mike", "testHang end")
}
//打印结果
testHang start
testHang end
testHang next
launch1 start
launch2 start
//两秒后
launch1 end
launch2 end
下面使用sleep
的案例中,会阻塞后续的执行,Coroutine 1
执行会阻塞两秒,然后才会执行Coroutine 2
fun testHang(){
Log.e("Mike", "testHang start")
GlobalScope.launch {
//Coroutine 1
GlobalScope.launch(Dispatchers.Main) {
Log.e("Mike", "launch1 start")
Thread.sleep(2000)
Log.e("Mike", "launch1 end")
}
Log.e("Mike", "testHang next")
//Coroutine 2
GlobalScope.launch(Dispatchers.Main) {
Log.e("Mike", "launch2 start")
Thread.sleep(2000)
Log.e("Mike", "launch2 end")
}
}
Log.e("Mike", "testHang end")
}
打印结果
testHang start
testHang end
testHang next
launch1 start
//两秒后
launch1 end
launch2 start
//两秒后
launch2 end
结构化并发
CoroutineScope
当我们每创建一个协程,就会有一个CoroutineScope
,它为协程的作用域可以直接使用this
引用或者省略
public interface CoroutineScope
结构化并发
结构化并发是一种编程范式,一种编写易读易维护并发程序的结构化方法。 结构化并发思想与结构化编程类似,即代码中并发任务有明确的入口和出口。 与超出当前代码作用域的并发任务相比,理解结构化并发代码更容易
线程的并发是非结构化的并发,因为当你创建多个线程的时候,他们之间并没有结构化的层级关系,所有线程的上下文都是整个进程
Kotlin
协程的并发是结构化,因为每个协程都有一个作用域CoroutineScope
,Scope
之间存在父子的结构化关系
后续
协程是一个庞大的系统,学习的过程也需要沉淀,下一步计划研究协程的下列问题
Job
suspend
协程的生命周期控制
协程与主线程的交互
协程与线程的选择
协程的应用
协程的单元测试
为什么会有协程
欢迎关注Mike的
Android知识整理