当我们执行异步任务时,通常会开启一个线程,但线程是由操作系统调度的,我们无法控制其调用时机。
协程也是用来执行异步任务的,我们可以通过在一个线程中开启多个协程的方式来执行异步任务,相比于线程,它更加轻量和快速。
使用协程前需要先导入库
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
协程必须运行在其作用域内,最简单的开启协程的方式就是在全局作用域中开启协程。
fun main() {
// 在全局作用域中开启协程
GlobalScope.launch {
println("coroutine scope")
}
}
但我们运行这段代码会发现没有任何输出,这是因为这样开启的协程不会阻塞主线程,并且协程会随着程序结束而停止。而协程开启又需要时间,所以代码来不及执行就停止了。所以我们可以等待一秒保证协程运行完毕。
fun main() {
GlobalScope.launch {
println("coroutine scope")
}
// 等待一秒钟保证协程运行完毕。
Thread.sleep(1000)
}
或者我们还可以通过 runBlocking 开启一个阻塞当前线程的协程,这样也能保证协程中的代码一定能够被执行。
fun main() {
// 开启一个阻塞当前线程的协程
runBlocking {
println("coroutine scope")
}
}
在协程作用域中,我们可以调用多次 launch 函数开启多个协程
fun main() {
runBlocking {
// 协程作用域中,可以多次调用 launch 函数开启多个协程,实现和开启多线程一样的并发效果
launch {
println("coroutine1")
// 挂起 1s
delay(1000)
println("coroutine1 finish")
}
launch {
println("coroutine2")
delay(1000)
println("coroutine2 finish")
}
}
}
运行程序,输出如下:
coroutine1
coroutine2
coroutine1 finish
coroutine2 finish
可以看出,多个协程之间确实是并发执行的。
在作用域中开启的协程,会随着外层作用域的协程结束而结束。
当我们需要在函数中使用协程的特性时,需要将函数声明为挂起函数
fun main() {
runBlocking {
test()
}
}
// 给函数添加 suspend 关键字,表示这是一个挂起函数。挂起函数只能在协程作用域中调用
private suspend fun test() {
// 挂起函数中可以使用协程的特性,比如这里的 delay 方法
delay(1000)
println("coroutine scope")
}
需要注意的是,挂起函数并不会继承外部的作用域,所以我们无法在挂起函数中直接调用 launch 方法。
在挂起函数中,我们可以使用 coroutineScope 函数继承外部作用域并开启一个子作用域
// 使用 coroutineScope 继承外部作用域并创建子作用域
private suspend fun test() = coroutineScope {
// 有了作用域,我们就可以使用 launch 开启新的协程
launch {
}
}
corountineScope 会阻塞当前协程
fun main() {
runBlocking {
coroutineScope {
launch {
println("coroutineScope start")
delay(1000)
println("coroutineScope finish")
}
}
println("runBlocking finish")
}
println("main finish")
}
运行程序,输出如下:
coroutineScope start
coroutineScope finish
runBlocking finish
main finish
launch 函数会返回一个 Job 对象,调用 Job 对象的 cancel 方法就能取消协程
val job = GlobalScope.launch {
}
job.cancel()
我们也可以构建一个 Job 对象,将 Job 对象传入 CoroutineScope 函数创建一个作用域。调用此 Job 对象的 cancel 方法时就能关闭此作用域下的所有协程。
// 创建一个 Job 对象
val job = Job()
// 将 Job 对象传入 CoroutineScope 函数创建一个作用域
val scope = CoroutineScope(job)
// 以此 scope 启动协程
scope.launch {
}
// cancel 时,所有 scope 作用域下的协程都将被取消
job.cancel()
在协程作用域中,使用 async 方法也可以启动一个协程,它还会返回一个 Deferred 对象,调用此对象的 await 方法就能获取执行结果。
runBlocking {
// result = 2
val result = async {
1 + 1
}.await()
}
async 函数开启协程时不会阻塞协程,但调用 await 函数获取结果时会阻塞协程,所以当使用多个 async 时,应该在最后统一调用 await 方法获取结果,使得协程可以并发执行,提高效率。
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
1 + 1
}.await()
val result2 = async {
delay(1000)
1 + 1
}.await()
// await 会阻塞协程,所以这种写法会导致两个协程串行执行,耗时 2 秒左右
println(System.currentTimeMillis() - start)
}
runBlocking {
val start = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
1 + 1
}
val deferred2 = async {
delay(1000)
1 + 1
}
// 统一调用 await 方法,让两个协程并行执行,耗时 1 秒左右,效率高
val result1 = deferred1.await()
val result2 = deferred2.await()
println(System.currentTimeMillis() - start)
}
}
async{}.await()
可以简写成 withContext(Dispatchers.Default)
runBlocking {
val start = System.currentTimeMillis()
val result1 = withContext(Dispatchers.Default) {
delay(1000)
1 + 1
}
val result2 = withContext(Dispatchers.Default) {
delay(1000)
1 + 1
}
// withContext 会阻塞协程,所以这种写法会导致两个协程串行执行,耗时 2 秒左右
println(System.currentTimeMillis() - start)
}
其中传入的 Dispatchers 用于指定协程运行在哪个线程中,有三个常用值:
其实 launch 函数和 async 也都可以指定 Dispatchers,只不过没有强制要求写而已。使用协程可以很轻松的切换线程,这也是它的优势之一:
runBlocking {
val result1 = async(Dispatchers.IO) {
// IO 操作
}.await()
val result2 = async(Dispatchers.Main) {
// 主线程操作
}.await()
}
实际上,由于 Jvm 的限制,Kotlin 在 Jvm 上是用线程池来实现的协程。也就是说,Kotlin 在 Jvm 上的协程等于帮助我们封装了一个线程池。
但是,协程和线程池是不能划等号的,协程本身是和线程同级别的概念,甚至是先进于线程的概念。协程设计的初衷是让我们无须开启多线程,只需在一个线程中开启多个协程就能实现并发。这样实现的并发的效率是高于多线程的。由于 Jvm 的设计规范所限,Kotlin 在 Jvm 上用线程池来实现协程实属无奈之举。并不代表所有的协程都是通过线程池实现的。
DSL 是指Domain Specific Language
,即 领域特定语言
,是指编程语言赋予开发者的一种能力,使得开发者可以编写出看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。
比如在 Android 项目中,build.gradle 中编写的代码实际上就是 Groovy 提供的 DSL 功能。
Kotlin 中使用 DSL 主要有两种方式:中缀函数和高阶函数。中缀函数我们在上一篇文章中已经讲过,接下来我们利用高阶函数,仿照 build.gradle 编写出与 gradle 中类似的语法结构。
新建 Dependency 类
class Dependency {
val libraries = ArrayList<String>()
fun implementation(lib: String) {
libraries.add(lib)
}
}
编写高阶扩展函数
fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libraries
}
这样就编写完成了,使用也很简单:
fun main() {
// 实际上就是调用了 dependencies 扩展函数,传入 Lambda 表达式
dependencies {
implementation("androidx.core:core-ktx:1.1.0")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:1.1.3")
}
}
到这里,我们的 Kotlin 教程就结束了,本系列主要是总结郭神在《第一行代码》(第三版)中分享的 Kotlin 知识,其中协程的部分参考了扔物线的视频教程,感兴趣的读者可以去阅读原文和观看原视频。