前言
在前面的文章中,已经讲解了Kotlin基础相关的知识点。从这一篇开始,将开始对Kotlin对应的协程进行详解!
话不多说,直接开始!
1、Kotlin基本使用
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val nameTextView = findViewById<TextView>(R.id.nameTextView)
nameTextView.text = "Jack"
val submitButton = findViewById<Button>(R.id.submitButton)
// submitButton.setOnClickListener(View.OnClickListener {
// Log.d("hqk","onClick")
// })
//上面那种有点java化,可以通过下面这种方式设置点击事件
submitButton.setOnClickListener{
Log.d("hqk","onClick")
}
}
}
这是一个非常简单MainActivity
里面包含对应的控件获取以及对应的事件添加。
到这里,相信从本专栏第一篇看到这的读者已经具备了,将原有Java项目转Kotlin项目的能力。
当然这并不是Kotlin的全部,Kotlin的最大亮点就是协程!也是一大难啃的骨头!但是香啊!
2、Kotlin协程初探
什么是协程?
官方描述:协程实际上是⼀个轻量级的线程,可以挂起并稍后恢复。协程通过挂起函数⽀持:对这样的函数的调⽤可能会挂 起协程,并启动⼀个新的协程,我们通常使⽤匿名挂起函数(即挂起 lambda 表达式)。
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
准备工作
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
需要在对应的Module里面注入对应的依赖!
2.1 示例一
fun main(){
//后台运行的新的协程
GlobalScope.launch {
delay(1000)
println("Kotlin!")
}
println("Hello,")
//协程已在等待时主线程还在继续,阻塞主线程2秒钟来保证JVM存活
Thread.sleep(2000)
}
运行效果
Hello, //这里 停顿一秒后才打印的下一句 Kotlin!
Kotlin!
这里我们看到,一开始就通过GlobalScope.launch{}
创建了一个协程,然后通过delay
挂起1了秒,最后再执行打印!
还是上面那句话:协程实际上是⼀个轻量级的线程,所以它还是一个线程,那使用java的thread
试试?
import kotlinx.coroutines.*
fun main(){
thread{
Thread.sleep(1000)
println("Kotlin!")
}
println("Hello,")
//协程已在等待时主线程还在继续,阻塞主线程2秒钟来保证JVM存活
Thread.sleep(2000)
}
运行效果
Hello, //这里 停顿一秒后才打印的下一句 Kotlin!
Kotlin!
这里我们看到,一开始就创建了thread
线程,然后通过Thread.sleep(1000)
阻塞了1,秒,接着再执行打印,最后我们看到执行结果和上面那种方式一致!
乍一看运行效果一样!
那么Thread.sleep(1000)
与delay(1000)
,它俩之间有何区别?
注意刚刚我在解释这两个方法的作用时,特意用了不同词:
-
delay(1000)
为挂起,它不会造成线程阻塞,但是会 挂起 协程,并且只能在协程中使用。 -
Thread.sleep(1000)
为阻塞,它会造成线程阻塞!
那么挂起与阻塞有何区别?(重点!重点!重点!)
- 挂起:一般是主动的,由系统或程序发出,甚至于辅存中去。(不释放CPU,可能释放内存,放在外存)
- 阻塞:一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待某种资源或信号量(即有了资源)将他唤醒。(释放CPU,不释放内存)
用客套话解释:
- 挂起:是车子把货卸了,但是车还在开,车有可能又在拖其他货物了
- 阻塞:是货没卸,车子停了,在等红绿灯,变绿灯了就走
不过第⼀个⽰例在同⼀段代码中混⽤了 ⾮阻塞的 delay(……) 与 阻塞的 Thread.sleep(……) 。这容易记混哪个是阻塞的、哪个是⾮阻塞的。那试试使⽤ runBlocking 协程构建器来阻塞:
2.2 示例二
import kotlinx.coroutines.*
fun main(){
GlobalScope.launch {
delay(1000)
println("Kotlin!")
}
println("Hello,")
//这个表达式阻塞了主线程,我们延迟 2 秒来保证 JVM 的存活
runBlocking {
delay(2000L)
}
}
运行效果
Hello, //这里 停顿一秒后才打印的下一句 Kotlin!
Kotlin!
结果是相似的,调用了 runBlocking{}
的主线程会一直阻塞直到 runBlocking 内部的协程执行完毕。
这个例子并不是很明显,只是为了过渡而使用!来看看下一个示例:
2.3 示例三
刚刚说了: 调用了 runBlocking{}
的主线程会一直阻塞直到 runBlocking 内部的协程执行完毕。
那么,将对应的协程包裹在runBlocking {}
里面试试?:
fun main() = runBlocking<Unit> { //开始执行主协程
GlobalScope.launch {
delay(1000L)
println("Kotlin!")
}
println("Hello,")
delay(2000L)
}
运行效果
Hello, //这里 停顿一秒后才打印的下一句 Kotlin!
Kotlin!
这⾥的 runBlocking <Unit> { …… }
作为⽤来启动顶层主协程的适配器。我们显式指定了其返回类型 Unit,因为在 Kotlin 中 main 函数必须返回 Unit 类型。<Unit>可省略
当然这样写会造成时间浪费,为啥要挂起2秒,可不可以把delay(2000L)
去掉!让协程执行完了自动结束
2.4 示例四
fun main() = runBlocking<Unit> { //开始执行主协程
val job:Job = GlobalScope.launch {
delay(1000L)
println("Kotlin!")
}
println("Hello,")
job.join() // 等待直到子协程执行结束
//....
}
运行效果
Hello, //这里 停顿一秒后才打印的下一句 Kotlin!
Kotlin!
这里额外定义一个变量job:Job
接收了对应的协程,随后通过job.join()
,一直等到子协程结束!
但这还是有缺陷:
当使⽤ GlobalScope.launch 时,这里会创建⼀个顶层协 程。虽然它很轻量,但它运⾏时仍会消耗⼀些内存资源。如果忘记保持对新启动的协程的引⽤,它还会继续 运⾏。如果协程中的代码挂起了会怎么样(例如,错误地延迟了太⻓时间),或者说启动了太多的协程导致内存不⾜会怎样?通过job.join()
,⼿动保持对所有已启动协程的引⽤也很容易出错。
所以,接着看下一个示例:
2.5 示例五
在⽰例中,使⽤ runBlocking 协程构建器将 main 函数转换为协程。包括 runBlocking 在内的 每个协程构建器都将 CoroutineScope 的实例添加到其代码块所在的作⽤域中。可以在这个作⽤域中启动协程⽽⽆需显式调用 join
函数,因为外部协程(⽰例中的 runBlocking )直到在其作⽤域中启动的所有协程都执⾏完毕后才会结束。因此,可以将示例简化为:
fun main() = runBlocking<Unit> {// this : CoroutineScope
launch {
delay(1000L)
println("Kotlin!")
}
println("Hello,")
}
运行效果
Hello, //这里 停顿一秒后才打印的下一句 Kotlin!
Kotlin!
除了由不同的构建器提供协程作用域之外,还可以使用 coroutineScope 构建器声明自己的作用域。
runBlocking
与coroutineScope
可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束。- 主要区别在于,runBlocking 方法会阻塞当前线程来等待,而 coroutineScope 只是挂起,会释放底层线程用于其他用途。
- 由于存在这点差异,runBlocking 是常规函数,而coroutineScope是挂起函数。
带着分析看看下一个示例:
2.6 示例六
fun main() = runBlocking<Unit> {
launch {
delay(200L)
println("Hello Kotlin")
}
// 创建一个协程作用域
coroutineScope {
launch { // 内嵌 launch
delay(500L)
println("内嵌 launch")
}
delay(100L)
// 这⼀⾏会在内嵌 launch 之前输出
println("自定义作用域")
}
// 这⼀⾏在内嵌 launch 执⾏完毕后才输出
println("runBlocking over")
}
运行效果
自定义作用域
Hello Kotlin
内嵌 launch
runBlocking over
注意,当等待内嵌 launch 时,紧挨“自定义作用域”消息之后,就会执⾏并输出“Hello Kotlin”——尽管 coroutineScope 尚未结束。
结束语
好了,本篇到这里差不多结束了。相信你对协程有了一定的基础认识。在下一篇中,将会讲解协程对应的取消超时时组合挂起函数。