上面这段简短的代码就是开启了一个协程,很简单吧,一行代码就实现了,协程也不过如此啊😂。实际下面这段代码背后包含着成吨的知识点:
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 {
/**
- Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
上述代码我们可以知道:GlobalScope 是一个单例类,实现了 CoroutineScope 这个东东,并重写了 coroutineContext 这个属性
1、CoroutineScope
接着点进去 CoroutineScope 这个东东看一下:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
1)、源码里面有一段对它的注释,翻译过来大致就是:CoroutineScope 能够定义一个协程作用域,每个协程构建器像 launch, async 都是它的一个扩展。
2)、它是一个接口,里面持有一个 CoroutineContext 即协程上下文,我们可以让类实现它,让该类成为一个协程作用域
2、GlobalScope
现在回到 GlobalScope 这个东东,我们应该可以把它解释清楚了:因为 GlobalScope 是一个单例类,且实现了CoroutineScope,所有它拥有了全局的协程作用域,且在整个 JVM 虚拟机中只有一份对象实例。因为它的生命周期贯穿整个 JVM,所以我们在使用它的时候需要警惕内存泄漏。上面代码中调用的 GlobalScope.launch,实质上是调用了 CoroutineScope 的 launch 扩展函数
3、协程作用域作用
那么这里你心里是否会有个疑问:拥有协程作用域有啥用呢?作用可大了
协程必须在协程作用域中才能启动,协程作用域中定义了一些父子协程的规则,Kotlin 协程通过协程作用域来管控域中的所有协程
协程作用域间可并列或包含,组成一个树状结构,这就是 Kotlin 协程中的结构化并发,规则如下:
4、作用域细分
有下述三种:
1)、顶级作用域:没有父协程的协程所在的作用域
2)、协同作用域:协程中启动新协程(即子协程),此时子协程所在的作用域默认为协同作用域,子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消;
3)、主从作用域:与协同作用域父子关系一致,区别在于子协程出现未捕获异常时不会向上传递给父协程
5、父子协程间的规则
1)、父协程如果取消或结束了,那么它下面的所有子协程均被取消或结束
2)、父协程需等待子协程执行完毕后才会最终进入完成状态,而不管父协程本身的代码块是否已执行完
3)、子协程会继承父协程上下文中的元素,如果自身有相同 Key 的成员,则覆盖对应 Key,覆盖效果仅在自身范围内有效
好了,到了这里关于协程作用域你是否理解了呢?如果不明白,接着往下看,或许随着学习的深入,你的问题就引刃而解了
四、使用 Delay 函数延迟协程执行
-
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 函数创建一个能阻塞当前线程的协程作用域
- 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 函数在当前的协程作用域下创建子协程
上面我们讲到过,launch 函数是 CoroutineScope 的一个扩展函数,因此只要拥有协程作用域,就可以调用 launch 函数
- 单独使用 launch 函数和我们刚才使用的 GlobalScope.launch 函数不同, GlobalScope.launch 创建的是一个顶级协程,而 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 关键字将一个函数声明成挂起函数,挂起函数之间是可以相互调用的
七、使用 suspend 关键字将一个函数声明成挂起函数
- suspend 关键字能将一个函数声明成挂起函数
- 挂起函数必须在协程或者另一个挂起函数里被调用
那么上面代码我们加个关键字修饰一下就 ok 了,如下:
suspend fun performLogistics(){
//处理成吨的逻辑代码
//…
delay(1500)
//…
}
现在问题又来了,如果我想在这个挂起函数中调用 launch 函数可以么?如下:
suspend fun performLogistics(){
//处理成吨的逻辑代码
//…
delay(1500)
//…
//这句代码编译器会报错,因为没有协程作用域
launch{
}
}
上面这段代码又报错了,因为没有协程作用域,那么如果我想这样调用,能实现么?
答:可以的,借助 coroutineScope 函数来解决
八、使用 coroutineScope 函数创建一个协程作用域
- coroutineScope 函数会继承外部的协程作用域并创建一个子作用域
- 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
从打印结果,我们就可以验证上面这一结论
九、使用 async 函数创建一个子协程并获取执行结果
从上面的学习我们可以知道 launch 函数可以创建一个子协程,但是 launch 函数只能用于执行一段逻辑,却不能获取执行的结果,因为它的返回值永远是一个 Job 对象,那么如果我们想创建一个子协程并获取它的执行结果,我们可以使用 async 函数
- async 函数必须在协程作用域下才能调用
- async 函数会创建一个子协程并返回一个 Deferred 对象,如果需要获取 async 函数代码块中的执行结果,只需要调用 Deferred 对象的 await() 方法即可
- async 函数在调用后会立刻执行,当调用 await() 方法时,如果代码块中的代码还没执行完,那么 await() 方法会将当前协程阻塞住,直到可以获取 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 函数
10、使用 withContext 函数构建一个简化版的 async 函数
- withContext 函数是一个挂起函数,并且强制要求我们指定一个协程上下文参数,这个调度器其实就是指定协程具体的运行线程
- withContext 函数在调用后会立刻执行,它可以保证其作用域内的所有代码和子协程在全部执行完之前,一直阻塞当前协程
- withContext 函数会创建一个子协程并将最后一行的执行结果作为返回值
fun main() {
runBlocking {
val result = withContext(Dispatchers.Default) {
5 + 5
}
println(result)
}
}
//打印结果
10
11、使用 suspendCoroutine 函数简化回调的写法
在日常工作中,我们通常会通过异步回调机制去获取网络响应数据,不知你有没有发现,这种回调机制基本上是依靠匿名内部类来实现的,比如如下代码:
sendHttpRequest(object : OnHttpCallBackListener{
override fun onSuccess(response: String) {
}
override fun onError(exception: Exception) {
}
})
那么在多少地方发起网络请求,就需要编写多少次这样的匿名内部类去实现,这样会显得特别繁琐。在我们学习 Kotin 协程之前,可能确实是没有啥更简单的写法了,不过现在,我们就可以借助 Kotlin 协程里面的 suspendCoroutine 函数来简化回调的写法:
- suspendCoroutine 函数必须在协程作用域或者挂起函数中调用,它接收一个 Lambda 表达式,主要作用是将当前协程立即挂起,然后在一个普通线程中去执行 Lambda 表达式中的代码
- suspendCoroutine 函数的 Lambda 表达式参数列表会传入一个 Contination 参数,调用它的 resume() 或 resumeWithException() 方法可以让协程恢复执行
//定义成功和失败的接口
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免费领取。