kotlin-协程

第一眼在kotlin官方文档中看到协程,哇什么东西,好像很厉害的样子。而文档中讲述线程之类的却不如它多,第一是因为kotlin可以运行在jvm中,自然是可以现成用java的并发库,所以也就简单的讲了讲。第二就是它希望开发者使用协程。关于协程的概念很多,有些语言有而有些没有。这里的话我们就只讲kotlin中的协程。

what and why:什么是协程,为什么用协程

关于名词解释有很多,比如官方文档:“本质上,协程是轻量级的线程”。还有“协作式”等等,乍一看,哦好像懂了。其实还是不懂,像极了学习计算机网络中的各种抽象的名词概念,而我们又拼命的想只知道这是什么,为什么叫协程。它和线程怎么比较。其实本质上名词只是指代一个具体事物的映射。所以当我们用抽象的东西去解释抽象东西,第一次真的想破头。所以我们不去管为啥叫协程,它的专业解释是什么。我们来看看它具体干了啥事,有什么用。有了具体事物就可以映射概念了。

1
2
3
4
5
6
fun main(args: Array<String>) {
GlobalScope.launch {
println("hello")
}
println("world")
}

这个协程的helloworld代码,有一个GlobalScope的单例对象。持有一个CoroutineContext上下文对象

1
2
3
public object GlobalScope : kotlinx.coroutines.CoroutineScope {
public open val coroutineContext: kotlin.coroutines.CoroutineContext
}

去调用扩展函数launch,下面的时lauchd的函数声明

1
2
3
4
5
6
7
public fun kotlinx.coroutines.CoroutineScope.launch(
context: kotlin.coroutines.CoroutineContext ,
start: kotlinx.coroutines.CoroutineStart ,
block: suspend kotlinx.coroutines.CoroutineScope.() -> kotlin.Unit): kotlinx.coroutines.Job
{

}

前两个参数应该是有默认参数(用idea看的没有看到源码)。我们launch括号后面的其实是一个lambda(函数字面值,表达式)承载着具体的执行代码。launch返回的是Job。并且执行代码。可以说lauch括号内执行这个过程就是一个协程。其实说白了就是一段程序的执行,加上一些特殊效果就被定义为协程。那么什么特殊的效果呢?

launch中的有CoroutineContext是可以指定协程运行的环境的。大家都知道进程是计算机资源分配的最小单位,线程是cpu执行的最小单位。而协程就是在线程上执行的有特殊效果的代码片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main(args: Array<String>) {
GlobalScope.launch(Dispatchers.Default) {
println("hello")
}
GlobalScope.launch(Dispatchers.Main) {
println("hello")
}
GlobalScope.launch(Dispatchers.IO) {
println("hello")
}
GlobalScope.launch(Dispatchers.Unconfined) {
println("hello")
}
println("world")
}

上述代码开启了四个协程。每个协程都运行在不同的线程环境中。

1
2
3
4
Dispatchers.Default//默认
Dispatchers.Main//主线程
Dispatchers.IO//IO线程
Dispatchers.Unconfined//计算线程

我们通过指定环境很轻松的就切换了线程。这就是协程的特性。在Android中我们常常因为耗时操作需要去将代码运行在IO线程,在运行完毕后需要切换回UI线程,然后又因为业务问题有需要切换。这样切来切去也是常有的事。但是切换通常需要大量的回调和不清晰的代码来执行。你可能想到了使用RxJava来实现比较方便的线程切换。而kotlin通过协程库帮我我们做到了类似RxJava的操作,虽然两者实现思想不一样,但是相对于原来的回调好用、清晰、方便。所以总结下来就是kotlin协程库就是方便我们操作线程的一个库,而kotlin协程则是一个运行在线程上的更小的代码单位

通俗的搞清楚了kotin协程是什么后,为什么用协程已经不言而喻。为了更加方便的线程切换,更加可阅读性代码。接下来就看看怎么用吧

how

添加依赖

作为额外的扩展依赖

1
2
3
4
5
6
7
8
dependencies {
...
// 👇 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1"
// 👇 依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1"
...
}

创建协程

首先我们需要取得CoroutineScope的实例,然后通过调用launch基本就就是一个运行协程的创建开启。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// 方法一,使用 runBlocking 顶层函数
runBlocking {
getImage(imageId)
}

// 方法二,使用 GlobalScope 单例对象
// 可以直接调用 launch 开启协程
GlobalScope.launch {
getImage(imageId)
}

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
// 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getImage(imageId)
}

第一种是会阻塞线,效果相当于Thread.sleep()的效果,适用于单元测试。

第二种的话使用了一个全局的scope对象,如果用这个的话,相当于把协程和全局的生命周期绑定在一起了。不能及时取消协程,不推荐。

第三种通过手动的方式构建。

指定运行线程

在开头的介绍的中说明了,在线程中存在很多的回调以及层级等待,线程切换。

1
2
3
coroutineScope.launch(Dispatchers.IO) {
...
}

设置协程的运行环境。

比如常见的Android中网络图片加载

1
2
3
4
5
6
coroutineScope.launch(Dispatchers.IO) {
val image = getImage(imageId)//加载图片
launch(Dispatch.Main) {
avatarIv.setImageBitmap(image)//切回到主线程执行协程
}
}

使用withContext指定环境

1
2
3
4
5
6
7
8
9
10
11
coroutineScope.launch(Dispachers.Main) {
...
withContext(Dispachers.IO) {
...
}
...
withContext(Dispachers.IO) {
...
}
...
}

但是目前还是将代码杂糅在一个地方,如果我们继续将带协程执行放在函数里面,这就成为了一个挂起函数

1
2
3
4
//👇
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
...
}

挂起的意思其实就是标志下,这里面有个协程会执行,并且可能要花一些时间的意思。调用这个协程的协程后续代码要待会才能继续执行。比如上菜的服务员告诉厨师做什么菜,然后厨师告诉他我要做10分钟,你才能来取菜。所以服务员就不能立马取菜。其实说白了就是线程切换等待通信知识,只不过操作更加简单,控制性更强了。

非阻塞式挂起

其实这玩意没啥玄乎的。通常我们在程序执行中,有耗时,有cpu,有IO操作,有UI操作。影响用户的是UI线程,所以我们将耗时操作放到其他的线程中执行。这样的感受就不那么卡了。而这里的非阻塞挂起其实就是通过线程切换调度完成的,手动的话也是可以实现,毕竟我们老干这事,不然就要ANR了。

参考扔物线的博客