1. 协程定义
从 Android 开发者的角度去理解它们的关系:
- 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
- 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
- 单线程中的协程总的执行时间并不会比不用协程少。
- Android 系统上,如果在主线程进行网络请求,会抛出
NetworkOnMainThreadException
,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。
协程设计的初衷是为了解决并发问题,让 「协作式多任务」 实现起来更加方便。这里就先不展开「协作式多任务」的概念,等我们学会了怎么用再讲。
视频里讲到,协程就是 Kotlin 提供的一套线程封装的 API,但并不是说协程就是为线程而生的。
2. 协程好在哪里
我们需要先了解一下「闭包」这个概念,调用 Kotlin 协程中的 API,经常会用到闭包写法。其实闭包并不是 Kotlin 中的新概念,在 Java 8 中就已经支持。
我们先以 Thread
为例,来看看什么是闭包:
// 创建一个 Thread 的完整写法
Thread(object : Runnable {
override fun run() {
...
}
})
// 满足 SAM,先简化为
Thread({
...
})
// 使用闭包,再简化为
Thread {
...
}
形如 Thread {...}
这样的结构中 {}
就是一个闭包。
在 Kotlin 中有这样一个语法糖:当函数的最后一个参数是 lambda 表达式时,可以将 lambda 写在括号外。这就是它的闭包原则。
在这里需要一个类型为 Runnable
的参数,而 Runnable
是一个接口,且只定义了一个函数 run
,这种情况满足了 Kotlin 的 SAM,可以转换成传递一个 lambda 表达式(第二段),因为是最后一个参数,根据闭包原则我们就可以直接写成 Thread {...}
(第三段) 的形式。
对于上文所使用的 launch
函数,可以通过闭包来进行简化 :
🏝️
launch {
...
}
3. 基本使用
launch
函数不是顶层函数,是不能直接用的,可以使用下面三种方法来创建协程:
🏝️
// 方法一,使用 runBlocking 顶层函数
runBlocking {
getImage(imageId)
}
// 方法二,使用 GlobalScope 单例对象
// 👇 可以直接调用 launch 开启协程
GlobalScope.launch {
getImage(imageId)
}
// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
// 👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getImage(imageId)
}
- 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
- 方法二和使用
runBlocking
的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会和 app 一致,且不能取消(什么是协程的取消后面的文章会讲)。 - 方法三是比较推荐的使用方法,我们可以通过
context
参数去管理和控制协程的生命周期(这里的context
和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。