概述
一个简单的线程框架。
优点
- 用看起来同步的方式,写出异步的代码,实现非阻塞式挂起。
- 可以把运行在不同线程的代码,写在一个代码块里,避免回调地狱。
最常见解决场景
比如A和B2个网络请求,都成功后才进行展示数据,但是如果使用原来回调来写,这2个必须是串行的,才能保证都得到了结果,这个写法就很垃圾。
协程可以把复杂的并行代码,写的更简单。
使用时机
当需要切线程或者指定线程时。
挂起
- 执行挂起时,协程就和线程脱离了,当前线程该干嘛干嘛,协程在指定线程里执行,执行完再切回来
- 挂起=稍后会被切回来的线程切换
- 挂起需要恢复,恢复是在协程里,所以挂起函数必须在协程里调用或者另一个挂起函数中调用。
- withContext就是挂起函数。
- suspend没有挂起作用,只是说这个函数是挂起函数,是提醒作用,是函数创建者对函数调用者的提醒,提醒我这个函数是耗时操作,需要在协程在运行。
非阻塞式挂起
非阻塞式就是上下2行代码可以在线程切走再切回来而已。
一些小例子
- 启动协程的是协程启动器,协程启动器不多,就几个,其中launch用的最多。
- launch是协程作用域的扩展方法,所以使用launch必须在协程作用域里。
- 一旦开启协程,就和主线程脱离了关系,主线程该干啥干啥去。
lifecycleScope.launch {
Log.i(TAG, "initView: 我要延迟")
Log.i(TAG, "initView: 我是协程")
delay(10000)
Log.i(TAG, "initView: 延迟结束")
launch {
Log.i(TAG, "initView: 我又开启了协程")
}
}
Log.i(TAG, "initView: 主线程继续跑别的")
- 使用runBlocking也可以开启协程,但是这个不是协程扩展函数。
- 既然它不是协程扩展函数,所以它会卡住当前线程。
runBlocking {
Log.i(TAG, "initView: 我可以卡主线程")
delay(5000)
Log.i(TAG, "initView: 延迟结束 好像gg 界面卡死")
}
- delay也是挂起函数。
- 定义挂起函数必须要使用suspend关键字,说明我这个是挂起函数,必须在协程里调用,谢谢。
private suspend fun delay10k(){
delay(10000)
Log.i(TAG, "deley10k: 延迟结束,我要开启协程")
}
既然我的挂起函数能在协程里调用,那我挂起函数再开启协程也合情合理喽,不过哪来的协程范围呢?这就需要看coroutineScope函数了,这个函数能创建一个协程范围从协程里,也就是子范围,还是很牛批的:
private suspend fun delay10k(){
delay(10000)
Log.i(TAG, "deley10k: 延迟结束,我要开启协程")
coroutineScope {
Log.i(TAG, "deley10k: 我也有作用域了 我要开启协程")
launch {
Log.i(TAG, "deley10k: 终于开启了 先延迟2s")
delay(2000)
Log.i(TAG, "delay10k: 延迟2s结束")
}
}
Log.i(TAG, "deley10k: 那我到底是先执行 还是等你2s结束呢 ")
}
这里的coroutineScope函数很有意思,它类似runblocking可以阻塞,runBlocking是阻塞当前线程,比如我代码在主线程执行,使用runBlocking开启的协程必须要执行完再执行线程;coroutineScope函数是阻塞协程,必须要等这个子范围内的协程执行完后,原来的协程才能继续执行。
说完了launch、runBlocking启动器,还有一个启动器就是async,从名字来看就是异步的意思,果然它很牛批。就比如2个耗时操作,使用这个可以直接并发开始,然后等待都有结果再处理或者其他条件处理,比在一个方法回调里启动另一个方法强多了。
runBlocking {
Log.i(TAG, "initView: 开始协程")
val start = System.currentTimeMillis()
val deferred = async {
Log.i(TAG, "initView: 我是A耗时任务 要开始了")
delay(1000)
Log.i(TAG, "initView: 我是A耗时任务,我结束了")
4 + 5
}
val deferred2 = async {
Log.i(TAG, "initView: 我是B耗时任务 要开始了")
delay(1000)
Log.i(TAG, "initView: 我是B耗时任务,我结束了")
8 +9
}
Log.i(TAG, "initView: 结果是${deferred.await()} ${deferred2.await()}")
val end = System.currentTimeMillis()
Log.i(TAG, "initView: 耗时 ${end - start}")
}
这里会发现时间耗时就1002秒,2个需要耗时各1s的居然在1s多都获取了结果,这个就很nice,如果其他方式来写的话就比较麻烦。
这个async方法的await方法是个阻塞方法,这啥意思呢,就是它会等待结果,但是异步任务在创建的时候就已经在跑了,调用await时只是获取其结果。比如在await前加个耗时:
runBlocking {
val deferred = async {
Log.i(TAG, "initView: 开始协程")
val start = System.currentTimeMillis()
Log.i(TAG, "initView: 我是A耗时任务 要开始了")
delay(1000)
Log.i(TAG, "initView: 我是A耗时任务,我结束了")
4 + 5
}
val deferred2 = async {
Log.i(TAG, "initView: 我是B耗时任务 要开始了")
delay(1000)
Log.i(TAG, "initView: 我是B耗时任务,我结束了")
8 +9
}
Log.i(TAG, "initView: 大家不必紧张,先暂停协程4s")
delay(4000)
Log.i(TAG, "initView: 结果是${deferred.await()} ${deferred2.await()}")
val end = System.currentTimeMillis()
Log.i(TAG, "initView: 耗时 ${end - start}")
}
查看打印:
2021-08-24 14:19:42.252 13422-13422/com.wayeal.yunapp I/zyh: initView: 开始协程 2021-08-24 14:19:42.252 13422-13422/com.wayeal.yunapp I/zyh: initView: 大家不必紧张,先暂停协程4s 2021-08-24 14:19:42.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是A耗时任务 要开始了 2021-08-24 14:19:42.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是B耗时任务 要开始了 2021-08-24 14:19:43.255 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是A耗时任务,我结束了 2021-08-24 14:19:43.255 13422-13422/com.wayeal.yunapp I/zyh: initView: 我是B耗时任务,我结束了 2021-08-24 14:19:46.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 结果是9 17 2021-08-24 14:19:46.254 13422-13422/com.wayeal.yunapp I/zyh: initView: 耗时 4002
会发现其实A B任务已经结束了,在调用await时才获取的值。
下面介绍一个非常重要的函数是withContext函数,它相当于是async+await结合在一块,其实也是启动协程的方法,把它看成协程启动器也不为过,但是必须指定调度器,而且这个方法会阻塞当前协程,这个特别关键,如果要异步还是得用async,直接看例子:
runBlocking {
Log.i(TAG, "initView: 开始协程")
val start = System.currentTimeMillis()
val result1 = withContext(Dispatchers.Default){
Log.i(TAG, "initView: 我是任务A 我要开始了")
delay(1000)
Log.i(TAG, "initView: 我是任务A 我结束了")
5 + 10
}
val result2 = withContext(Dispatchers.Default){
Log.i(TAG, "initView: 我是任务B 我要开始了")
delay(1000)
Log.i(TAG, "initView: 我是任务B 我结束了")
9 + 1
}
Log.i(TAG, "initView: 结果是${result1} ${result2}")
val end = System.currentTimeMillis()
Log.i(TAG, "initView: 耗时 ${end - start}")
}
打印结果:
2021-08-24 14:30:43.481 13611-13611/com.wayeal.yunapp I/zyh: initView: 开始协程 2021-08-24 14:30:43.485 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务A 我要开始了 2021-08-24 14:30:44.487 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务A 我结束了 2021-08-24 14:30:44.488 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务B 我要开始了 2021-08-24 14:30:45.489 13611-13645/com.wayeal.yunapp I/zyh: initView: 我是任务B 我结束了 2021-08-24 14:30:45.489 13611-13611/com.wayeal.yunapp I/zyh: initView: 结果是15 10 2021-08-24 14:30:45.489 13611-13611/com.wayeal.yunapp I/zyh: initView: 耗时 2008
从打印结果可以发现,先执行的A 再执行的B,2个耗时任务是串行的。
总结
协程的东西其实并不多,先了解其就是一个线程操作的API库,然后挂起就是切线程,执行完还会给切回来这个本质就好了。关于启动协程就是launch、async、runBlocking、withContext、coroutineScope等方法的使用,根据具体业务调用方法即可。