_Kotlin_系列_ 三、Kotlin协程(上)

那么上面代码我们加个关键字修饰一下就 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) {
//对异常情况进行处理
}
}

上述代码中:

1、我们在 request 函数内部使用了刚刚介绍的 suspendCoroutine 函数,这样当前协程会立刻被挂起,而 Lambda 表达式中的代码则会在普通线程中执行。接着我们在 Lambda 表达式中调用了 sendHttpRequest() 方法发起网络请求,并通过传统回调的方式监听请求结果

2、如果请求成功就调用 Continuation 的 resume() 方法恢复被挂起的协程,并传入服务器响应的数据,该值会成为 suspendCoroutine 函数的返回值

3、如果请求失败,就调用 Continuation 的 resumeWithException() 方法恢复被挂起的协程,并传入具体的异常原因

4、最后在 getBaiduResponse() 中进行了具体使用,有没有觉得这里的代码清爽了很多?由于 getBaiduResponse() 是一个挂起函数,当 getBaiduResponse() 调用了 request() 函数时,当前协程会立刻挂起,然后等待网络请求成功或者失败后,当前协程才能恢复运行

5、如果请求成功,我们就能获得异步网络请求的响应数据,如果请求失败,则会直接进入 catch 语句中

到这里其实又会产生一个问题:getBaiduResponse() 函数被声明成了一个挂起函数,因此它只能在协程作用域或其他挂起函数中调用了,使用起来是不是非常有局限性?

答:确实如此,因为 suspendCoroutine 函数本身就是要结合协程一起使用的,这个时候我们就需要通过合理的项目架构设计去解决这个问题

经过上面的步骤,我们使用 suspendCoroutine 函数实现了看似同步的方式写出异步的代码,事实上 suspendCoroutine 函数几乎可以用于简化任何回调的写法,例如我们在实际项目中使用 Retrofit 就可以使用 suspendCoroutine 函数来简化回调

到了这里,相信你对协程有了一定的了解了,接下来,我们分析一点深入的东西

十二、Kotlin 中的挂起操作

挂起算是 Kotlin 协程中的一个黑魔法了,上面我们简单了介绍了下使用 suspend 定义一个挂起函数,下面我们来详细的去剖析一下 Kotlin 中的挂起操作

1、挂起的本质

如下代码:

class MainActivity : AppCompatActivity() {

private val TAG: String = “MainActivity”

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

Log.d(TAG, "start… ");
GlobalScope.launch(Dispatchers.Main) {
mockTimeConsume()
Log.d(TAG, “我等挂起函数执行完了在执行”);
}
Log.d(TAG, “我在主线程执行了成吨的代码”);
}

//模拟挂起函数耗时任务
suspend fun mockTimeConsume() = withContext(Dispatchers.IO){
Log.d(TAG, "紧张的执行耗时任务中… " + + Thread.currentThread().name);
Thread.sleep(3000)
}
}
//打印结果如下
start…
我在主线程执行了成吨的代码
紧张的执行耗时任务中…DefaultDispatcher-worker-2
我等挂起函数执行完了在执行

上述代码步骤:

1、在主线程中创建了一个顶级协程,并指定该协程在主线程中运行

2、在协程中执行 mockTimeConsume 这个方法并打印了一句 Log

现在我们从线程和协程两个角度去分析它:

前面我在回答问题的时候讲到过,挂起就是切换到另外一个指定的线程去执行

线程

线程:那么当执行到协程中的 mockTimeConsume() 这句代码的时候,因为遇到了挂起函数,协程被挂起了,主线程将会跳出这个协程,如果下面还有代码,则继续执行下面的代码,如果没有,则执行它界面刷新的任务

协程

协程:当执行到协程中的 mockTimeConsume() 这句代码的时候,因为遇到了挂起函数,当前协程会被挂起,注意是整个协程被挂起了,意味着 mockTimeConsume() 这句代码下面的代码都不会执行了,需等待我这句代码执行完之后在接着往后执行,接下来会在指定的线程执行挂起函数里面的内容。谁指定的?是当前挂起函数指定的,比如我们这个例子中,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程

Dispatchers 调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行

常用的 Dispatchers ,有以下三种:

  • Dispatchers.Main:Android 中的主线程
  • Dispatchers.IO:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求
  • Dispatchers.Default:适合 CPU 密集型的任务,比如计算

当挂起函数执行完之后,协程为我们做的最爽的事就来了:恢复当前协程,把线程从其他线程,切回到了当前的线程。那么接着就会执行协程中 Log.d(TAG, “我等挂起函数执行完了在执行”) 这句代码,整个流程就结束了

通过上面对线程和协程两个角度都分析,我们可以得出一些结论:

1、被 suspend 修饰的挂起函数比普通函数多两个操作:

1)、挂起:暂停当前协程的执行,保存所有的局部变量

2)、恢复:从协程被暂停的地方继续执行协程

2、协程在执行到有 suspend 标记的挂起函数时,会被挂起,而所谓的被挂起,就是切换线程

3、协程被挂起之后需要恢复,而恢复这个操作是协程框架给我们做的

通过结论 3 ,我们引申一下:如果你不在协程里面调用挂起函数,恢复这个功能没法实现,所以也就回答了问题:为什么挂起函数必须在协程或者另一个挂起函数里被调用

再细想下这个逻辑:一个挂起函数要么在协程里被调用,要么在另一个挂起函数里被调用,那么它其实直接或者间接地,总是会在一个协程里被调用的

所以,要求 suspend 函数只能在协程里或者另一个 suspend 函数里被调用,还是为了要让协程能够在挂起函数切换线程之后再切回来

2、是怎么被挂起的?

到这里你心里是否会有另外一个疑问:协程是怎么被挂起的?如果上面那个挂起函数这么写:

suspend fun mockTimeConsume(){
Log.d(TAG, “紧张的执行耗时任务中…” + Thread.currentThread().name);
Thread.sleep(3000)
}

运行后你会发现打印的线程是主线程,那为什么没有切换线程呢?因为它不知道往哪切,需要我们告诉它,之前我们是这么写的:

suspend fun mockTimeConsume() = withContext(Dispatchers.IO){
Log.d(TAG, "紧张的执行耗时任务中… " + + Thread.currentThread().name);
Thread.sleep(3000)
}

我们可以发现不同之处其实在于 withContext 函数。

其实通过 withContext 源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher 参数,依赖这个 Dispatcher 参数的指示,你的协程就被挂起了,然后切到别的线程

所以使用 suspend 定义的挂起函数,还不是真正的挂起函数,真正的挂起函数内部需要调用到 Kotlin 协程框架自带的挂起函数

因此我们想要自己写一个挂起函数,仅仅只加上 suspend 关键字是不行的,还需要函数内部直接或间接地调用到 Kotlin 协程框架自带的 挂起函数才行

3、使用 suspend 的意义

通过上面的分析我们知道,使用 suspend 关键字修饰的函数可能还不是一个真正的挂起函数,那它的作用是啥呢?

起到一个提醒的作用,提醒调用者我是一个耗时函数,需要在挂起函数或者协程中调用我

为什么 suspend 关键字并没有实际去操作挂起,但 Kotlin 却把它提供出来?

因为它本来就不是用来操作挂起的。

挂起的操作 —— 也就是切线程,依赖的是挂起函数里面的实际代码,而不是这个关键字

所以这个关键字,只是一个提醒

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后说一下我的学习路线

其实很简单就下面这张图,含概了Android所有需要学的知识点,一共8大板块:

  1. 架构师筑基必备技能
  2. Android框架体系架构(高级UI+FrameWork源码)
  3. 360°Androidapp全方位性能调优
  4. 设计思想解读开源框架
  5. NDK模块开发
  6. 移动架构师专题项目实战环节
  7. 移动架构师不可不学习微信小程序
  8. 混合开发的flutter

Android学习的资料

我呢,把上面八大板块的分支都系统的做了一份学习系统的资料和视频,大概就下面这些,我就不全部写出来了,不然太长了影响大家的阅读。需要的小伙伴可以私信我【进阶】我免费分享给大家,或者直接点击下面链接领取,谢谢大家这么久以来的支持。

Android学习PDF+架构视频+面试文档+源码笔记

如果你有其他需要的话,也可以在GitHub上查看,下面的资料也会陆续上传到Github

330页PDF Android学习核心笔记(内含上面8大板块)

Android学习的系统对应视频

总结

我希望通过我自己的学习方法来帮助大家去提升技术:

  • 1、多看书、看源码和做项目,平时多种总结

  • 2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理

  • 3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习

  • 4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!

    进阶学习资料领取方式:GitHub

中…(img-oPqj4JmM-1710675240175)]

Android学习的系统对应视频

总结

我希望通过我自己的学习方法来帮助大家去提升技术:

  • 1、多看书、看源码和做项目,平时多种总结

  • 2、不能停留在一些基本api的使用上,应该往更深层次的方向去研究,比如activity、view的内部运行机制,比如Android内存优化,比如aidl,比如JNI等,并不仅仅停留在会用,而要通过阅读源码,理解其实现原理

  • 3、同时对架构是有一定要求的,架构是抽象的,但是设计模式是具体的,所以一定要加强下设计模式的学习

  • 4、android的方向也很多,高级UI,移动架构师,数据结构与算法和音视频FFMpeg解码,如果你对其中一项比较感兴趣,就大胆的进阶吧!

    进阶学习资料领取方式:GitHub

希望大家多多点赞,转发,评论加关注,你们的支持就是我继续下去的动力!加油!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值