协程是什么?
首先kotlin协程是kotlin的扩展库(kotlinx.coroutines)。
上一篇我们简单了解了线程的概念,线程在Android开发中一般用来做一些复杂耗时的操作,避免耗时操作阻塞主线程而出现ANR的情况,例如IO操作就需要在新的线程中去完成。但是呢,如果一个页面中使用的线程太多,线程间的切换是很消耗内存资源的,我们都知道线程是由系统去控制调度的,所以线程使用起来比较难于控制。这个时候kotlin的协程就体现出它的优势了,kotlin协程是运行在线程之上的,它的切换由程序自己来控制,无论是 CPU 的消耗还是内存的消耗都大大降低。所以大家赶紧来拥抱kotlin协程吧_
为Android项目中引入kotlin协程
- 添加依赖
首先要确保你的kotlin版本在1.1以上,我们在Android module中的build.gradle的dependencies中添加如下依赖。
api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
这里我使用的kotlin版本为1.3.50,协程库版本为1.3.2
- 添加混淆
在混淆代码中,具有不同类型的字段可以具有相同的名称,并且AtomicReferenceFieldUpdater可能无法找到正确的字段。要避免在混淆期间按类型进行字段重载,请将其添加到配置中:
-keepclassmembernames class kotlinx.** {
volatile <fields>;
}
回想一下刚学 Thread 的时候
我相信现在接触 Kotlin 的开发者绝大多数都有 Java 基础,我们刚开始学习 Thread 的时候,一定都是这样干的:
val thread = object : Thread(){
override fun run() {
super.run()
//do what you want to do.
}
}
thread.start()
肯定有人忘了调用 start,还特别纳闷为啥我开的线程不启动呢。说实话,这个线程的 start 的设计其实是很奇怪的,不过我理解设计者们,毕竟当年还有 stop 可以用,结果他们很快发现设计 stop 就是一个错误,因为不安全而在 JDK 1.1 就废弃,称得上是最短命的 API 了吧。
既然 stop 是错误,那么总是让初学者丢掉的 start 是不是也是一个错误呢?
哈,有点儿跑题了。我们今天主要说 Kotlin。Kotlin 的设计者就很有想法,他们为线程提供了一个便捷的方法:
val myThread = thread {
//do what you want
}
这个 thread 方法有个参数 start 默认为 true,换句话说,这样创造出来的线程默认就是启动的,除非你实在不想让它马上投入工作:
val myThread = thread(start = false) {
//do what you want
}
//later on ...
myThread.start()
这样看上去自然多了。接口设计就应该让默认值满足 80% 的需求嘛。
为什么我们要使用协程
上面我们简单介绍协程的设计巧妙的避开了Thread的弊端,但是协程的作用究竟是什么呢?为啥我们的整个项目要直接用协成替换了Thread?它又是如何能够在如此大的项目中直接扮演Thread这么重要的角色?
首先来强调一个概念:协程是一个轻量级的线程。
接下来用一个官方的demo,解释一下协程为什么能够被如此重视:
runBlocking{
repeat(100_000){//循环100000次
launch{//开启一个协程
delay(1000L)
print(".")
}
}
}
案例很简单,开启10万个协程。等等?启动10万个??没错!这里可以很顺畅的启动10万个!这里我们想想,如果我们启动10万个Thread会是什么样子呢?从这点来看,协程的确可以称的上轻量级。那么协程的优点仅此而已吗?不着急,我们一点点来看。
初识协程:
首先我们来瞄一眼协程是长啥样的, 以下引用(copy)了官网的一个例子:
fun main(args: Array<String>) {
launch(CommonPool) {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
运行结果: ("Hello,"会立即被打印, 1000毫秒之后, "World!"会被打印)
Hello,
World!
姑且不管里面具体的细节, 上面代码大体的运行流程是这样的:
- 主流程:
1、调用系统的launch方法启动了一个协程, 跟随的大括号可以看做是协程体.
(其中的CommonPool暂且理解成线程池, 指定了协程在哪里运行)
2、打印出"Hello,"
3、主线程sleep两秒
(这里的sleep只是保持进程存活, 目的是为了等待协程执行完)
- 协程流程:
协程延时1秒
打印出"World!"
解释一下delay方法:
在协程里delay方法作用等同于线程里的sleep, 都是休息一段时间, 但不同的是delay不会阻塞当前线程, 而像是设置了一个闹钟, 在闹钟未响之前, 运行该协程的线程可以被安排做了别的事情, 当闹钟响起时, 协程就会恢复运行.
再看协程:
我们可以把协程认为是一个轻量的线程。像线程一样,协程同样可以并行运行,彼此等待并进行通信。协程和线程最大的不同就是,协程很轻量,我们可以创建上千个,并且只消耗很少的性能。线程从开始到保持都要耗费很多资源,而且对现在机器来说上千个线程是一个很严峻的挑战。我们可以通过launch{}方法开启一个协程,默认情况下协程运行在一个共享的线程池上。线程仍然可以运行在一个基于协程开发的程序中,一个线程可以运行很多个协程,所以我们将不再需要很多的线程。示例如下:
import kotlinx.coroutines.experimental.*
fun main(args: Array<String>) {
println("Start")
// Start a coroutine
launch {
delay(1000)
println("Hello")
}
Thread.sleep(2000) // wait for 2 seconds
println("Stop")
}
运行结果:
说明:上述代码中我们开启了一个协程,一秒后打印hello。我们使用delay()方法,就像使用Thread.sleep()方法,但是delay方法会更好一些,它不会阻塞线程,它只是暂停协程本身。当协程正在等待时,线程返回到池中,并且当等待完成时,协程将在池中的空闲线程上恢复。
如果你想在main函数中使用非阻塞的delay方法,会发生一个编译错误Suspend functions are only allowed to be called from a coroutine or another suspend function,因为我们没有在协程中执行,我们将它包装在runBolcking{}中使用。runBlocking{}会启动协程并等待协程执行完成
import kotlinx.coroutines.experimental.*
fun main(args: Array<String>) {
println("Start")
// Start a coroutine
launch {
delay(1000)
println("Hello1")
}
runBlocking {
delay(2000)
println("Hello2")
}
Thread.sleep(2000) // wait for 2 seconds
println("Stop")
}
kotlin协程的三种启动方式
到这里,已经我们已经看了两个简单的协程案例了,这两个案例我们都是用了launch关键字来启动协程,其实协程有三种通用的启动方式
-
runBlocking:T
-
launch:Job
-
async/await:Deferred
第一种启动方式(runBlocking:T)
runBlocking 方法用于启动一个协程任务,通常只用于启动最外层的协程,例如线程环境切换到协程环境。
上图是官方源码中给出的该方法的解释,意思就是说runBlocking启动的协程任务会阻断当前线程,直到该协程执行结束。
代码示例:
执行结果:可以清楚的看到先将协程中的任务完成才执行主线程中的逻辑
第二种启动方式(launch:Job)
我们最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。例如join()启动一个协程、cancel() 取消一个协程
注⚠️:该方式启动的协程任务是不会阻塞线程的
代码示例:
执行结果:可以清楚的看到主线程没有被阻塞
第三种启动方式(async/await:Deferred)
1.async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。
2.async用于启动一个异步的协程任务,await用于去得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。
代码示例:
执行结果:可以看到当协程任务执行完毕时可以通过await()拿到返回结果
虽然有三种启动方式,但是大部分情况,我们都是使用Launch这种方式。
协程是可以被取消的和超时控制,可以组合被挂起的函数,协程中运行环境的指定,也就是线程的切换
协程启动后还可以取消
launch方法有一个返回值, 类型是Job, Job有一个cancel方法, 调用cancel方法可以取消协程, 看一个数羊的例子:
fun main(args: Array<String>) {
val job = launch(CommonPool) {
var i = 1
while(true) {
println("$i little sheep")
++i
delay(500L) // 每半秒数一只, 一秒可以输两只
}
}
Thread.sleep(1000L) // 在主线程睡眠期间, 协程里已经数了两只羊
job.cancel() // 协程才数了两只羊, 就被取消了
Thread.sleep(1000L)
println("main process finished.")
}
运行结果是:
1 little sheep
2 little sheep
main process finished.
如果不调用cancel, 可以数到4只羊。
在开发过程中通过cancel我们可以及时释放不必要的资源。