协程是什么?
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码,协程这个概念几十年前就有了,但是协程只是在近年才开始兴起,应用的语言有:go 、goLand、kotlin、python , 都是支持协程的
协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
在 Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。使用协程可以大幅度的提升Android开发者的工作效率。
协程能做什么?
在Android中,协程是Google在 Android 上进行异步编程的推荐解决方案。
轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作。
fun main() {
runBlocking {
val job = GlobalScope.launch {
delay(1000)
println("hello, ")
}
println("world !")
job.join() //等待直到协程执行结束
}
}
内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。
fun main() {
runBlocking {
val job = launch{
repeat(1000){i->
println("job: I'm sleeping $i")
delay(500L)
}
}
delay(1300L)
println("main: I'm tired of waiting")
job.cancel()
job.join()
println("main: Now I'm quit")
}
}
job: I’m sleeping 0 … job: I’m sleeping 1 … job: I’m sleeping 2 … main: I’m tired of waiting! main: Now I can
quit.
Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
1)ViewModelScope
2)LifecycleScope
3)liveData
大家可以参考这个链接:
协程可以做的事情如下:
- 处理耗时任务 (Long running tasks),这种任务常常会阻塞住主线程;
- 保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数。
处理耗时任务
Android 中的每个应用都会运行一个主线程,它主要是用来处理 UI (比如进行界面的绘制) 和协调用户交互。如果主线程上需要处理的任务太多,应用运行会变慢,看上去就像是 “卡” 住了,这样是很影响用户体验的。所以想让应用运行上不 “卡”、做到动画能够流畅运行或者能够快速响应用户点击事件,就得让那些耗时的任务不阻塞主线程的运行。
要做到处理网络请求不会阻塞主线程,一个常用的做法就是使用回调。回调就是在之后的某段时间去执行您的回调代码。然而Callback会让我们陷入回调嵌套陷阱,因为我们往往需要在回调函数中做某些事件,比如下面的伪代码:
场景: 要登录某个地方,我们需要先获取token,然后再获取token后在回调callback里面执行登录,登录成功后我们再
拿到 loginbean,拿到bean后我们再进行进行显示,这个过程中间就会出现好几次回调
getToken() {
callback1()->{
//do something
login(){
callback2()->{
//do something
loginBean() ->{
}
}
}
}
}
loadAndCombine(String name1,String name2){
//同步请求获取image1
image1 = getImageEnqueue(name1);
//同步请求获取image2
iamge2 = getImageEnqueue(name2);
//基于iamge1 &image2的结果合并新的image并返回
return combineImages(image1, image2)
}
以上代码的嵌套,或许大家可以用rxjava来解决。但是,kotlin的 协程可以天生的帮我们解决,kotlin可以拿到每一步的结果后再进行接下来的操作,kotlin的代码如下:
runBlocking{// 协程
launch {
val token = getToken() // 挂起函数
val loginState=getTokenlogin(token)// 使用上一步挂起函数的结果,同时在调用挂起函数进行网络请
求
val loginBean = getBean(loginState)// 继续使用上一步的挂起函数结果,进行异步请求
}
suspend fun getToken()= withCotext(Dispatchers.IO){/**/} //挂起函数,访问服务器
suspend fun getTokenlogin(toke)= withCotext(Dispatchers.IO) //挂起函数,访问服务器
suspend fun getBean(loginState)= withCotext(Dispatchers.IO) //挂起函数,访问服务器
大家可以看到上面的挂起函数,每个挂起函数都在进行异步访问服务器,而且,都需要使用上一步异步操作的结果,那么大家可能会有很多疑问,难道它不会阻塞主线程吗?getXXX方法是如何做到不等待网络请求和线程阻塞而返回结果的?其实,是 Kotlin 中的协程提供了这种执行代码而不阻塞主线程的方法。
协程在常规函数的基础上新增了两项操作。在 invoke (或 call) 和 return 之外,协程新增了 suspend 和 resume:
suspend — 也称挂起或暂停,用于暂停执行当前协程,并保存所有局部变量;
resume — 用于让已暂停的协程从其暂停处继续执行。
Kotlin 通过新增 suspend 关键词来实现上面这些功能。 suspend 函数只能被另外的 suspend 函数调用,或者通过协程构造器 (如 launch) 来启动新的协程。为什么? 后面有专门介绍挂起函数的时候解释
在下面的示例中,get 仍在主线程上运行,但它会在启动网络请求之前暂停协程。当网络请求完成时,get 会恢复已暂停的协程,而不是使用回调来通知主线程。
观察上图中 fetchDocs 的执行,就能明白 suspend 是如何工作的。Kotlin 使用堆栈帧来管理要运行哪个函数以及所有局部变量。暂停协程时,会复制并保存当前的堆栈帧以供稍后使用。恢复协程时,会将堆栈帧从其保存位置复制回来,然后函数再次开始运行。在上面的动画中,当主线程下所有的协程都被暂停,主线程处理屏幕绘制和点击事件时就会毫无压力。所以用上述的 suspend 和 resume 的操作来代替回调看起来十分的清爽。
当主线程下所有的协程都被暂停,主线程处理别的事件时就会毫无压力。
即使代码可能看起来像普通的顺序阻塞请求,协程也能确保网络请求避免阻塞主线程。
保证主线程安全
在 Kotlin 的协程中,主线程调用编写良好的 suspend 函数通常是安全的。不管那些 suspend 函数是做什么的,它们都应该允许任何线程调用它们。但是在我们的 Android 应用中有很多的事情处理起来太慢,是不应该放在主线程上去做的,比如网络请求、解析JSON 数据、从数据库中进行读写操作,甚至是遍历比较大的数组。这些会导致执行时间长从而让用户感觉很 “卡” 的操作都不应该放在主线程上执行。使用 suspend 并不意味着告诉 Kotlin 要在后台线程上执行一个函数,这里要强调的是,协程会在主线程上运行。事实上,当要响应一个 UI 事件从而启动一个协程时,使用Dispatchers.Main.immediate 是一个非常好的选择,这样的话哪怕是最终没有执行需要保证主线程安全的耗时任务,也可以在下一帧中给用户提供可用的执行结果。
协程会在主线程中运行,suspend 并不代表后台执行。
如果需要处理一个函数,且这个函数在主线程上执行太耗时,但是又要保证这个函数是主线程安全的,那么您可以让Kotlin 协程在 Default 或 IO 调度器上执行工作。在 Kotlin 中,所有协程都必须在调度器中运行,即使它们是在主线程上运行也是如此。协程可以自行暂停,而调度器负责将其恢复。
Kotlin 提供了四个调度器,您可以使用它们来指定应在何处运行协程:
Dispatchers.Main : Android 上的主线程,只能在Android中运行。用于UI交互和一些轻量级任务
应用方向: 调用suspend函数,调用UI函数,更新LiveData
Dispatchers.IO:非主线程,专为磁盘和网络IO进行优化
应用方向:数据库,文件读写,网络处理
Dispatchers.Default 非主线程, 对CPU密集任务进行优化
应用方向: 数组排序,JSON数据解析
Dispatchers.Unconfined 就是不指定线程,官方建议,一般情况不要用
在 Android 平台上,您可以使用协程来处理两个常见问题:
- 简化处理类似于网络请求、磁盘读取甚至是较大 JSON 数据解析这样的耗时任务;
- 保证主线程安全,这样可以在不增加代码复杂度和保证代码可读性的前提下做到不会阻塞主线程的执行
Coroutine是「非阻塞」的挂起
协程所写的代码和java中写的线程的代码的方式是很类似的,所以协程在写法上看起来是「阻塞」的,但其实它是「非阻塞」的,因为在协程里面它做了很多工作,其中有一个就是帮我们切线程。
main {
GlobalScope.launch(Dispatchers.Main) {
// 耗时操作
val user = suspendingRequestUser()
updateView(user)
}
private suspend fun suspendingRequestUser() : User = withContext(Dispatchers.IO) {
api.requestUser()
}
}
从上面的例子可以看到,耗时操作和更新 UI 的逻辑像写单线程一样放在了一起,只是在外面包了一层协程。而正是这个协程解决了原来我们单线程写法会卡线程这件事。
阻塞的内涵
举个例子,当你开发的 app 在性能好的手机上很流畅,在性能差的老手机上会卡顿,就是在说同一行代码执行的时间不一样。
视频中讲了一个网络 IO 的例子,IO 阻塞更多是反映在「等」这件事情上,它的性能瓶颈是和网络的数据交换,你切多少个线程都没用,该花的时间一点都少不了。
而这跟协程半毛钱关系没有,切线程解决不了的事情,协程也解决不了。
阻塞」与「非阻塞」只有在一个线程的时候才有它的存在意义,而协程一个非常重要的工作就是切线程,也就是说协程是多个线程的共同协调工作,那么它就不会存在阻塞的概念了,因此协程是非阻塞的挂起