Hi你好,新同学。很高兴,你终于追寻这个问题了,也许你正感到迷茫,各路大神对协程的理解不一,有人说它是线程框架,有人说它比线程更轻,希望我这篇博文可以帮你从另一个角度简单理解协程。
请相信一句话,任何解释从第二个人口中说出时,可能已经存在了变化。而官网是我们接触任何技术最必要的门槛。所以请打开Kotlin中文网。很多人说kotlin官网教程很不详细,其实不然,kotlin中文网教程很详细。
回到正题:
什么是协程?
- 异步编程
- 体验
- 语言级
- 理念
注意上面几个关键点和一些实际使用,不难明白
Kotlin协程是基于Kotlin语法从而延伸的一个异步编程框架,它并没有带来多少性能上的体验,它能实现的,你用线程池同样也可以实现,但对于使用角度的来说,协程努力打造一个 "同步方式,异步编程的" 思想,作为开发者来说,我们可以更懒了,切换线程,withContext即可,协程带来了开发上的舒适,但这种舒适是基于 Kotlin 的语法,并不是别的。所以我希望大家刚入手协程时,多从语言角度去理解。
那么,协程是什么?
协程就是一个基于Kotlin语法的异步框架,它可以使开发者以同步的方式,写成异步的代码,而无需关注多余操作。就这么简单
协程怎么用?
启动一个协程
代码语言:javascript复制suspend fun main() {
GlobalScope.launch {
println("启动--${System.currentTimeMillis()}---${Thread.currentThread().name}")
delay(100)
println("挂起100ms--${System.currentTimeMillis()}---${Thread.currentThread().name}")
}
delay(1000)
}
代码语言:javascript复制启动--1579085375166---DefaultDispatcher-worker-1
挂起100ms--1579085375275---DefaultDispatcher-worker-1
并发使用
代码语言:javascript复制fun main() {
runBlocking(Dispatchers.IO) {
println("启动--${System.currentTimeMillis()}")
val as1 = async {
delay(100)
println("并发1--${System.currentTimeMillis()}---${Thread.currentThread().name}")
"1"
}
val as2 = async {
delay(100)
println("并发2--${System.currentTimeMillis()}---${Thread.currentThread().name}")
"2"
}
println("as1=${as1.await()},as2=${as2.await()}")
}
}
代码语言:javascript复制启动--1579085205199
并发1--1579085205313---DefaultDispatcher-worker-3
并发2--1579085205313---DefaultDispatcher-worker-2
as1=1,as2=2
观察上面demo的运行结果,是不是很舒服,看起来同步的方式内部却是在异步操作。那上面注释中 挂起 是什么意思呢?
什么是挂起?
观察上面的打印日志,我们不难发现,在调用 delay 函数时,线程并没有停下,相对来说,只是我们的协程代码块被挂起,等待恢复。只有前面的挂起函数执行结束,我们的协程代码块才能继续执行。借用一幅图来说明如下:
所以所谓的挂起其实是代码层次的一个处理,从而使得我们可以以同步形式去写异步的代码。
非阻塞程序?
所谓的非阻塞,其实就是切换了线程,观察打印日志变化,我们可以发现,当我们直接 GlobalScope.launch 启动一个协程时,此时运行的线程为默认的线程,所以协程被称为非阻塞的实现方式。
suspend关键字的作用
先看下面的图
当使用suspend修饰后的函数我们称其为挂起函数,那么挂起函数有什么作用?为什么test 的suspend 标志是黑的?
继续看截图
挂起函数为什么只能在挂起函数中使用呢??
我们再继续往下看:看一下java字节码
这个 Continuation是什么呢?按照字面意思,意思为延续。那我们该怎么理解呢?
一般来说 Continuation 代表的是<剩余的计算的概念>,也就是 接下来要执行的代码。官方的解释叫做 挂起点
比如我有一段代码:
代码语言:javascript复制println("123".length)
最先执行的是"123".length,然后执行 println打印长度,此时执行 “123”.length 就可以当做一个挂起点,也就是代码从这里停止,等待计算出结果,然而此时内部线程却没有停止,当计算完的时候,也就是挂起结束,此时接着执行我们的打印语句。
切换到我们的suspend中,它代表的就是一个标志 的作用,有suspend修饰的代表的函数叫做挂起函数,当编译器碰到这个标志的函数,就知道它是一个可能会耗时的操作。
挂起函数为什么只能在挂起函数中使用呢?
因为普通函数参数并没有带 Continuation啊,相当于没有挂起点,编译器无法判断,所以此时会报错。
为什么test 的suspend 标志是黑的?
编译器知道它是挂起函数,但是test内部没有挂起函数啊,所以此时编译器提示。 那这个函数就一点作用没有? 有作用,就是限制这个函数只能挂起函数调用。
在Android中使用
倒计时功能
代码语言:javascript复制class Main3Activity : AppCompatActivity() {
private val mainScope = CoroutineScope(Dispatchers.Default)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main3)
progressBar.max = 100
mainScope.launch {
(1..100).forEach {
delay(100)
withContext(Dispatchers.Main) {
progressBar.secondaryProgress = it
}
}
}
}
onDestory(){
//记得关闭
}
}
结合 ViewModel 使用
代码语言:javascript复制class MainViewModel : ViewModel() {
fun test() {
viewModelScope.launch {
}
}
}
在ViewModel取消时,协程将自动关闭
结合 Lifecycle 使用
导入以下依赖 implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03"
于是刚才的倒计时代码改为如下:
代码语言:javascript复制class Main3Activity : AppCompatActivity() {
....
lifecycleScope.launch {
...
}
}
}
同样,当fragment或者Activity关闭时,协程同样将自动关闭。
查看源码,会发现,viewModel中的 viewModelScope 和 Lifecycle lifecycleScope,实现方式如出一辙:
本篇,我们没有过多的从源码上去追寻,协程到底是什么,尽量从语法,使用者的角度入手,带着大家从侧面去理解协程,希望大家都能有自己的理解。