1. 概念
Kotlin的协程机制是Kotlin的一大特色,主要用于处理形形色色的异步任务,并优化其性能,简化其操作
在现在的Android开发中,将协程与Retrofit进行结合,运用到各种需要异步加载的场景当中,Kotlin作为Goolgle官方推荐的语言,正在逐步发挥其光热,而协程则是其中的利器之一
kotlinx.coroutines作为官方的重要程序库提供了协程的支持
2. 项目创建
首先,还是先创建一个项目,当然使用Koltin语言,使用Gradle进行构建,因为这个Android里用得比较多,DSL可以随便选,主要也是一些固定API
项目创建完成之后,需要对于依赖进行一个配置,由于使用的DSL是Kotlin,这边生成的文件就成了build.gradle.kts了,这是项目构建的配置文件
需要在依赖的代码块中加入对于Kotlin协程核心库的依赖
dependencies {
testImplementation(kotlin("test"))
// 协程的核心依赖
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
}
接下来同步Gradle,会到远程仓库下载依赖到本地
3. 基础
3.1. 初识协程
协程是一个可挂起计算的实例,在概念上与线程相近,利用一个代码块和其他的代码并发执行
但是,协程不与特定的线程绑定,可能会在线程A中挂起,随后又在线程B中恢复执行
可以将协程看作是轻量级线程,但是二者之间也存在巨大差异,使得二者在实际使用上大不相同
先编写最简单的协程代码,观察并运行一下
fun main() = runBlocking { // 协程域(前面提到的代码块)
launch {
delay(1_000L) // 非阻塞延时
println("World!")
}
println("Hello") // 主协程输出
}
最终的输出先输出Hello,然后等待约1s,再输出World!
3.2. 剖析
上面的代码中包含了协程的几个要素,接下来意义理解一番
launch是协程的builder,它发起一个新的协程,该协程与其余的代码并发执行,并且独立工作,因而Hello首先输出,它就是所谓的其余代码
delay是一个特殊的挂起函数
它会将当前的协程挂起一段指定的时间
挂起协程不会阻塞底层的线程,但是允许其他的协程为自己的代码运行并使用底层的线程
runBlocking也是一个协程的builder,在这里的使用是将main()(非协程部分)与runBlocking{}包裹的协程代码进行了连接
在Idea的环境中对应会有协程作用域的提示,对应的正是刚介绍的两个协程builder方法
如果将当前的runBlocking代码块移除掉,那么代码就会直接报错
定位到launch{}代码块上查看一下其定义相关的信息,可以发现launch实际上是一个扩展函数,需要其调用者是CoroutineScope类型,因此runBlocking{}用以提供这样的一个环境,没有这样的环境,就不能直接使用launch{}
接下来继续分析runBlocking{}
调用runBlocking{}的线程(在这里就是主线程),会在调用的期间一直阻塞,直到runBlocking{}代码块中的协程代码全部执行完毕
runBlocking{}通常用于应用程序的顶层(这里我的理解是应用程序各功能模块的调用方),而很少用于真实的业务代码的内部;这是因为线程是很昂贵的资源,如果阻塞它们代码执行会非常低效,并且通常情况下也不会有这样的需求
3.3. 结构化并发
协程都遵循一个原则——结构化并发
结构化并发的含义就是新的协程只能在一个指定的协程域,即CoroutineScope之上创建,而创建新协程的这个协程域,也就决定了新协程的存活时间
根据之前的例子,runBlocking{}创建了CoroutineScope,因此在其上创建协程的launch{}的存活时间被拿捏了,在协程域中创建一个协程去打印World!,同时也打印Hello;先打印Hello,但是并没有结束,因为协程要等待1s执行World!,完成后程序退出
既然结构化并发采用这种代码块嵌套的方式来编写,那么似乎也是采用一种树形结构的方式将协程进行关联和管理,有点类似文件树和视图树结构
在实际的应用开发中,可能需要启动许多的协程进行业务执行,如何有条不紊地进行管理就需要依赖于结构化并发,因为这能确保它们不会丢失或者引起内存的泄漏
外层的协程域需要等它的所有子级协程全部执行完毕,方可结束
结构化并发也可以确保任何代码中的错误都可以被正确报告,不会遗漏
3.4. 提取函数,进行重构
对launch{}的代码进行提取,会生成一个带有suspend标记的函数,这便是挂起函数
挂起函数在协程中可以像非协程环境中的常规函数那样调用
不同的是,在其中使用其他挂起函数可以挂起当前协程执行
fun main() = runBlocking {
launch {
print()
}
println("Hello")
}
// 提取生成挂起函数
private suspend fun print() {
delay(1_000L)
println("World!")
}
3.5. 作用域建造器(Scope Builder)
除了提供的不同的协程域建造器,也可以由开发者借助coroutineScope建造器进行自定义
coroutineScope将会创建一个协程作用域,并且直至由其发起的所有协程全部完成才会结束
runBlocking{}和coroutineScope{}可能在看上去比较相似,这二者都会等待它们内部的代码以及子级的协程执行完毕才会结束
二者主要的不同在于runBlocking{}会阻塞当前的线程,用以等待内部任务执行完毕;而coroutineScope{}仅仅是挂起,它会释放底层的线程用作其他用途
正是由于这点不同,runBlocking{}仅仅是一个常规函数,而coroutineScope{}是一个挂起函数
fun main() = runBlocking {
coroutineScope { // 挂起函数,包裹原逻辑,结果其实一样
launch {
delay(1_000L)
println("World!")
}
println("Hello")
}
}
3.6. 作用域建造者与并发
coroutineScope{}可以在任意的挂起函数内部进行调用,进行多个并发操作
fun main() = runBlocking {
coroutineScope {
launch { // 启动协程1
delay(1_000L)
println("World!")
}
launch { // 启动协程2
delay(3_000L)
println("...")
}
println("Hello")
}
println("Over") // 全部结束执行
}
首先,在coroutineScope{}中有三个并发操作,两个launch{}以及本身的Hello,因为没有延时,先打印Hello,随后1s后打印World!,再过2s打印...,等到全部结束之后,位于runBlocking{}中的Over被打印
3.7. Job对象
launch{}协程建造器会返回一个Job类型的对象,它会持有被创建的协程,通常可以用于等待特定的协程完成
fun main() = runBlocking {
coroutineScope {
val job = launch {
delay(1_000L)
println("World!")
}
job.join() // 等待Job对应的协程执行完
println("Hello")
}
println("Over")
}
通过Job对象对应launch{}的内容,调用join()等待代码块执行完毕,原本直接执行的Hello被迫等待1s,让World!先执行完
3.8. 协程是轻量的
协程使用的资源密度要低于同等情况下的线程
一些耗尽资源的线程操作换做协程表示,并不会超出限制
fun main() = runBlocking {
repeat(100_000) {
launch { // 协程并不会消耗太多
delay(5_000L)
print(".")
}
}
}
fun main() = runBlocking {
repeat(100_000) {
thread { // 换成线程就很卡顿
Thread.sleep(5_000L)
print(".")
}
}
}
协程短短几秒就输出结果,并且内存上涨起伏很小(从原来500M ~ 550+M吧)
而线程运行了很长时间,而且内存增长很快,被反复回收(从500M ~ 700+M,然后被回收了(只分配了750M))