《第三行代码》Kotlin 总结(四)

九、协程

当我们执行异步任务时,通常会开启一个线程,但线程是由操作系统调度的,我们无法控制其调用时机。
协程也是用来执行异步任务的,我们可以通过在一个线程中开启多个协程的方式来执行异步任务,相比于线程,它更加轻量和快速。

9.1 导入

使用协程前需要先导入库

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

9.2 协程作用域

协程必须运行在其作用域内,最简单的开启协程的方式就是在全局作用域中开启协程。

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

可以看出,多个协程之间确实是并发执行的。

在作用域中开启的协程,会随着外层作用域的协程结束而结束。

9.3 挂起函数

当我们需要在函数中使用协程的特性时,需要将函数声明为挂起函数

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

9.4 协程的取消

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()

9.5 获取协程执行结果

在协程作用域中,使用 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 用于指定协程运行在哪个线程中,有三个常用值:

  • Dispatchers.Default:用于计算密集型任务,低并发的线程策略
  • Dispatchers.IO:用于 IO 密集型任务,高并发线程策略
  • Dispatchers.Main:在 Android 主线程中执行任务

其实 launch 函数和 async 也都可以指定 Dispatchers,只不过没有强制要求写而已。使用协程可以很轻松的切换线程,这也是它的优势之一:

runBlocking { 
    val result1 = async(Dispatchers.IO) { 
        // IO 操作
    }.await()
    val result2 = async(Dispatchers.Main) { 
        // 主线程操作
    }.await()
}

9.6 Kotlin 在 Jvm 上协程的本质

实际上,由于 Jvm 的限制,Kotlin 在 Jvm 上是用线程池来实现的协程。也就是说,Kotlin 在 Jvm 上的协程等于帮助我们封装了一个线程池。

但是,协程和线程池是不能划等号的,协程本身是和线程同级别的概念,甚至是先进于线程的概念。协程设计的初衷是让我们无须开启多线程,只需在一个线程中开启多个协程就能实现并发。这样实现的并发的效率是高于多线程的。由于 Jvm 的设计规范所限,Kotlin 在 Jvm 上用线程池来实现协程实属无奈之举。并不代表所有的协程都是通过线程池实现的。

十、DSL

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 知识,其中协程的部分参考了扔物线的视频教程,感兴趣的读者可以去阅读原文和观看原视频。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值