上面这段简短的代码就是开启了一个协程,很简单吧,一行代码就实现了,协程也不过如此啊。实际下面这段代码背后包含着成吨的知识点:
1、协程作用域
2、协程作用域的扩展函数
3、协程上下文
4、协程启动模式
可能大家会有点疑惑,区区一行代码,怎么可能会涉及这么多东西?不信我们在点击 launch 函数看下它的源码:
// launch 函数源码
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//…
}
可以看到,launch 函数是 CoroutineScope 即协程作用域的一个扩展函数,它里面有三个参数:第一个参数: CoroutineContext 即协程上下文,有默认值。第二个参数: CoroutineStart 即协程启动模式,有默认值。第三个参数:函数类型参数,无默认值。因此 launch 函数在实际调用的时候,只需要传入一个 Lambda 表达式就可以了,当然你也可以传参去覆盖默认值
好了,知道它里面涉及到这么多知识点,现在我们来进行各个击破,下面我会讲解协程作用域,其他的在这篇文章分析可能有点枯燥,我们放到下篇文章在来分析
回到最开始那段代码,首先我们看到 GlobalScope 这个东东,点进去看一眼它的源码:
public object GlobalScope : CoroutineScope {
/**
上述代码我们可以知道:GlobalScope 是一个单例类,实现了 CoroutineScope 这个东东,并重写了 coroutineContext 这个属性
接着点进去 CoroutineScope 这个东东看一下:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
1)、源码里面有一段对它的注释,翻译过来大致就是:CoroutineScope 能够定义一个协程作用域,每个协程构建器像 launch, async 都是它的一个扩展。
2)、它是一个接口,里面持有一个 CoroutineContext 即协程上下文,我们可以让类实现它,让该类成为一个协程作用域
现在回到 GlobalScope 这个东东,我们应该可以把它解释清楚了:因为 GlobalScope 是一个单例类,且实现了CoroutineScope,所有它拥有了全局的协程作用域,且在整个 JVM 虚拟机中只有一份对象实例。因为它的生命周期贯穿整个 JVM,所以我们在使用它的时候需要警惕内存泄漏。上面代码中调用的 GlobalScope.launch,实质上是调用了 CoroutineScope 的 launch 扩展函数
那么这里你心里是否会有个疑问:拥有协程作用域有啥用呢?作用可大了
协程必须在协程作用域中才能启动,协程作用域中定义了一些父子协程的规则,Kotlin 协程通过协程作用域来管控域中的所有协程
协程作用域间可并列或包含,组成一个树状结构,这就是 Kotlin 协程中的结构化并发,规则如下:
有下述三种:
1)、顶级作用域:没有父协程的协程所在的作用域
2)、协同作用域:协程中启动新协程(即子协程),此时子协程所在的作用域默认为协同作用域,子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消;
3)、主从作用域:与协同作用域父子关系一致,区别在于子协程出现未捕获异常时不会向上传递给父协程
1)、父协程如果取消或结束了,那么它下面的所有子协程均被取消或结束
2)、父协程需等待子协程执行完毕后才会最终进入完成状态,而不管父协程本身的代码块是否已执行完
3)、子协程会继承父协程上下文中的元素,如果自身有相同 Key 的成员,则覆盖对应 Key,覆盖效果仅在自身范围内有效
好了,到了这里关于协程作用域你是否理解了呢?如果不明白,接着往下看,或许随着学习的深入,你的问题就引刃而解了
delay 函数是一个非阻塞式挂起函数,它可以让当前协程延迟到指定的时间执行,且只能在协程的作用域或者其他挂起函数中调用
对比 Thread.sleep() 函数,delay 函数只会挂起当前协程,并不会影响其他协程的运行,而 Thread.sleep() 函数会阻塞当前线程,那么该线程下的所有协程都会被阻塞
fun main() {
GlobalScope.launch {
println(“codes run in coroutine scope”)
}
}
上述代码你运行一下会发现日志打印不出来,小朋友,你是否有很多问号?
这是因为代码块中的代码还没来得及执行,应用程序就结束了,要解决这个问题,我们可以让程序延迟一段时间在结束,如下:
fun main() {
GlobalScope.launch {
println(“codes run in coroutine scope”)
}
Thread.sleep(1000)
}
//打印结果
codes run in coroutine scope
上述代码我们让主线程阻塞了 1 秒钟在执行,因此代码块中的代码得到了执行。其实这种写法还是存在一点问题,如果我让代码块中的代码在 1 秒钟内不能运行结束,那么就会被强制中断:
fun main() {
GlobalScope.launch {
println(“codes run in coroutine scope”)
delay(1500)
println(“codes run in coroutine scope finished”)
}
Thread.sleep(1000)
}
//打印结果
codes run in coroutine scope
上述代码我们在代码块中加入了一个 delay 函数,并在其之后又打印了一行日志。那么当前协程会挂起 1.5 秒,而主线程却只阻塞了 1 秒,那么重新运行一下程序,新增的这条日志并没有打印出来,因为它还没来得及运行,程序就结束了。
那有办法让协程中所有的代码都执行完了之后在结束吗?️
答:有的,使用 runBlocking 函数
注意:runBlocking 函数通常只能在测试环境中使用,在正式环境中使用会容易产生一些性能上的问题
fun main() {
runBlocking {
println(“codes run in coroutine scope”)
delay(1500)
println(“codes run in coroutine scope finished”)
}
}
//打印结果
codes run in coroutine scope
codes run in coroutine scope finished
上述代码我们使用了 runBlocking 函数,可以看到两条日志都能够正常打印出来了。到了这里我心里会有一个疑问:上面的代码都是跑在同一个协程中,我能不能创建多个协程同时跑呢?
答:可以的,使用 launch 函数
上面我们讲到过,launch 函数是 CoroutineScope 的一个扩展函数,因此只要拥有协程作用域,就可以调用 launch 函数
fun main() {
runBlocking {
launch {
println(“launch1”)
delay(1000)
println(“launch1 finished”)
}
launch {
println(“launch2”)
delay(1000)
println(“launch2 finished”)
}
}
}
//打印结果
launch1
launch2
launch1 finished
launch2 finished
上述代码我们调用了两次 launch 函数,也就是创建了两个子协程,运行之后我们可以看到两个子协程的日志是交替打印的,这一现象表明他们像是多线程那样并发运行的。然而这两个子协程实际上是运行在同一个线程中,只是由编程语言来决定如何在多个协程之间进行调度,让谁运行,让谁挂起。调度的过程完全不需要操作系统参与,这也就使得协程的并发效率出奇的高
目前 launch 函数中的逻辑是比较简单的,那么随着逻辑越来越多,我们可能需要将部分代码提取到一个单独的函数中,如下:
fun performLogistics(){
//处理成吨的逻辑代码
//…
//这句代码编译器会报错,因为 delay 函数只能在协程作用域或者其他挂起函数中调用
delay(1500)
//…
}
上面这段代码报错了,因为提取到一个单独的函数中就没有协程作用域了,那么 delay 函数就调用不了了,蛋疼,有没有其他办法呢?
仔细分析一下,我们知道 delay 函数只能在协程作用域或者其他挂起函数中调用,现在提取出来的单独函数没有协程作用域了,那么是否可以把它声明成一个挂起函数呢?
答:可以的,使用 suspend 关键字将一个函数声明成挂起函数,挂起函数之间是可以相互调用的
那么上面代码我们加个关键字修饰一下就 ok 了,如下:
suspend fun performLogistics(){
//处理成吨的逻辑代码
//…
delay(1500)
//…
}
现在问题又来了,如果我想在这个挂起函数中调用 launch 函数可以么?如下:
suspend fun performLogistics(){
//处理成吨的逻辑代码
//…
delay(1500)
//…
//这句代码编译器会报错,因为没有协程作用域
launch{
}
}
上面这段代码又报错了,因为没有协程作用域,那么如果我想这样调用,能实现么?
答:可以的,借助 coroutineScope 函数来解决
suspend fun printDot() = coroutineScope {
println(".")
delay(1000)
launch {
}
}
上述代码调用 launch 函数就不会报错了。
另外, coroutineScope 函数和 runBlocking 函数有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,一直阻塞当前协程。而 runBlocking 是一直阻塞当前线程,我们来做个验证:
fun main() {
runBlocking {
coroutineScope {
launch {
for (i in 1…5) {
println(i)
}
}
}
println(“coroutineScope finished”)
}
println(“runBlocking finished”)
}
//打印结果
1
2
3
4
5
coroutineScope finished
runBlocking finished
从打印结果,我们就可以验证上面这一结论
从上面的学习我们可以知道 launch 函数可以创建一个子协程,但是 launch 函数只能用于执行一段逻辑,却不能获取执行的结果,因为它的返回值永远是一个 Job 对象,那么如果我们想创建一个子协程并获取它的执行结果,我们可以使用 async 函数
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val result1 = async {
delay(1000)
5 + 5
}.await()
val result2 = async {
delay(1000)
4 + 6
}.await()
println(“result is ${result1 + result2}”)
val end = System.currentTimeMillis()
println(“cost: ${end - start} ms.”)
}
}
//打印结果
result is 20
cost: 2017 ms.
上述代码连续使用了两个 async 函数来执行任务,并在代码块中进行 1 秒的延迟,按照刚才上面说的,await() 方法在 async 函数代码块中的代码执行完之前会一直将当前协程阻塞住。整段代码的执行耗时是 2017 ms,说明这里的两个 async 函数确实是一种串行的关系,前一个执行完了下一个才能执行。很明显这种写法是比较低效的,因为两个 async 完全可以异步去执行,而现在却被整成了同步,我们改造一下上面的写法:
fun main() {
runBlocking {
val start = System.currentTimeMillis()
val deferred1 = async {
delay(1000)
5 + 5
}
val deferred2 = async {
delay(1000)
4 + 6
}
println(“result is ${deferred1.await() + deferred2.await()}”)
val end = System.currentTimeMillis()
println(“cost: ${end - start} ms.”)
}
}
//打印结果
result is 20
cost: 1020 ms.
上面的写法我们没有在每次调用 async 函数之后就立刻使用 await() 方法获取结果了,而是仅在需要用到 async 函数的执行结果时才调用 await() 方法进行获取,这样 async 函数就变成了一种异步关系了,可以看到打印结果也验证了这一点
我是个喜欢偷懒的人, async 函数每次都要调用 await() 方法才能获取结果,比较繁琐,那我就会想:有没有类似 async 函数并且不需要每次都去调用 await() 方法获取结果的函数呢?
答:有的,使用 withContext 函数
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}
//打印结果
10
在日常工作中,我们通常会通过异步回调机制去获取网络响应数据,不知你有没有发现,这种回调机制基本上是依靠匿名内部类来实现的,比如如下代码:
sendHttpRequest(object : OnHttpCallBackListener{
override fun onSuccess(response: String) {
}
override fun onError(exception: Exception) {
}
})
那么在多少地方发起网络请求,就需要编写多少次这样的匿名内部类去实现,这样会显得特别繁琐。在我们学习 Kotin 协程之前,可能确实是没有啥更简单的写法了,不过现在,我们就可以借助 Kotlin 协程里面的 suspendCoroutine 函数来简化回调的写法:
//定义成功和失败的接口
interface OnHttpCallBackListener{
fun onSuccess(response: String)
fun onError(exception: Exception)
}
//模拟发送一个网络请求
fun sendHttpRequest(url: String, httpCallBack: OnHttpCallBackListener){
}
//对发送的网络请求回调使用 suspendCoroutine 函数进行封装
suspend fun request(url: String): String{
return suspendCoroutine { continuation ->
sendHttpRequest(url,object : OnHttpCallBackListener{
override fun onSuccess(response: String) {
continuation.resume(response)
}
override fun onError(exception: Exception) {
continuation.resumeWithException(exception)
}
})
}
}
//具体使用
suspend fun getBaiduResponse(){
try {
val request = request(“https://www.baidu.com/”)
} catch (e: Exception) {
//对异常情况进行处理
}
}
上述代码中:
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长。而不成体系的学习效果低效漫长且无助。时间久了,付出巨大的时间成本和努力,没有看到应有的效果,会气馁是再正常不过的。
所以学习一定要找到最适合自己的方式,有一个思路方法,不然不止浪费时间,更可能把未来发展都一起耽误了。
如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。以上知识笔记全部免费分享,如有需要获取知识笔记的朋友,可以点击我的GitHub免费领取。