一、介绍
协程就是个线程框架,用同步的方式写异步的代码
二、使用
1.添加依赖
在项目中配置对 Kotlin 协程的支持
在build.gradle
文件中增加对Kotlin协程的依赖:
- 项目根目录下的
build.gradle
buildscript {
...
ext.kotlin_coroutines = '1.3.1'
...
}
- Module 下的
build.gradle
dependencies {
...
//依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
//依赖当前平台所对应的平台库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
...
}
- 核心库和平台库的版本应该保持一致
- 核心库中包含的代码主要是协程的公共 API 部分。有了这一层公共代码,才使得协程在各个平台上的接口得到统一
- 平台库中包含的代码主要是协程框架在具体平台的具体实现方式。因为多线程在各个平台的实现方式是有所差异的
2.基础使用
//使用runBlocking顶层函数
runBlocking {
//耗时代码
}
//使用GlobalScope单例对象,调用launch开启协程
GlobalScope.launch {
//耗时代码
}
//通过CoroutineContext创建一个 CoroutineScope 对象
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
//耗时代码
}
- runBlocking:适用于单元测试的场景,线程阻塞的
- GlobalScope.launch:和
runBlocking
的区别在于不会阻塞线程。它的生命周期会和app一致,且不能取消,不推荐使用 - CoroutineScope(context):可以通过
context
参数去管理和控制协程的生命周期
可以使用 Dispatchers.IO
参数把任务切到 IO 线程执行:
coroutineScope.launch(Dispatchers.IO) {
...
}
使用 Dispatchers.Main
参数切换到主线程执行:
coroutineScope.launch(Dispatchers.Main) {
...
}
使用kotlin协程进行网络请求,切换到主线中更新UI
coroutineScope.launch(Dispatchers.Main) { // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
nameTv.text = user.name // 主线程更新 UI
}
多个网络请求需要等待所有请求结束之后再对 UI 进行更新
coroutineScope.launch(Dispatchers.Main) {
val avatar = async { api.getAvatar(user) } // 获取用户头像
val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
val merged = suspendingMerge(avatar, logo) // 合并结果
show(merged) // 更新 UI
}
这个launch函数是:
创建一个新的协程,并在指定的线程上运行它,传给 launch
的那些代码,就叫做协程
协程的使用场景:需要切线程或者指定线程的时候
3.withContext的使用
withContext:可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行
原始代码(还是有嵌套):
coroutineScope.launch(Dispatchers.IO) {
val image = getImage(imageId)
launch(Dispatchers.Main) {
avatarIv.setImageBitmap(image)
}
}
结合withContext的使用
coroutineScope.launch(Dispatchers.Main) { // 在UI线程开始
val image = withContext(Dispatchers.IO) { // 切换到IO线程,并在执行完成后切回UI线程
getImage(imageId) // 将会运行在IO线程
}
avatarIv.setImageBitmap(image) // 回到UI线程更新UI
}
可以把 withContext
放进一个单独的函数里面:
launch(Dispatchers.Main) { //在UI线程开始
val image = getImage(imageId)
avatarIv.setImageBitmap(image) //执行结束后,自动切换回UI线程
}
fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
...
}
编译器会报错,withContext
是一个 suspend
函数,它需要在协程或者是另一个 suspend
函数中调用
launch
与async的异同点:
相同点:
都可以用来启动一个协程,返回的都是
Coroutine
不同点:
async
返回的Coroutine
多实现了Deferred
接口,延迟获取
async的使用案例:
coroutineScope.launch(Dispatchers.Main) { //async 函数启动新的协程 val avatar: Deferred = async { api.getAvatar(user) } // 获取用户头像 val logo: Deferred = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo //获取返回值 show(avatar.await(), logo.await()) // 更新 UI }
而这个await() 就是用suspend关键字修饰的
4.suspend的使用
是协程中最核心的关键字,暂停或者可挂起的意思,代码执行到 suspend
函数的时候会『挂起』,并且这个挂起是非阻塞式的,它不会阻塞你当前的线程。
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
...
}
挂起的对象是协程
启动一个协程可以使用launch
或者async
函数,协程就是这两个函数中闭包的代码块
launch、
async
或者其他函数创建的协程,在执行到 suspend
函数时,这个协程会被suspend,也就是被挂起。从当前线程挂起。也就是这个协程从正在执行它的线程上脱离,这个线程不再去管这个协程要做什么了,
接下来线程就去做它该做的事情,没事情做的时候也可以被回收
而协程在suspend
函数指定的线程中继续执行,在 suspend
函数执行完成之后,会自动把线程在切回来
举例:
// 主线程中
GlobalScope.launch(Dispatchers.Main) {
val image = suspendingGetImage(imageId) // 获取图片
avatarIv.setImageBitmap(image) // 显示出来
}
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
这段执行在主线程的协程,它实质上会往你的主线程 post
一个 Runnable
,这个 Runnable
就是你的协程代码。
等协程代码块执行完成之后,会自动切回来,相当于在post
一个 Runnable,
让剩下的代码继续回到主线程去执行
Dispatchers
调度器
它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行
常用的
Dispatchers
调度器:
Dispatchers.Main
:Android 中的主线程Dispatchers.IO
:针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求Dispatchers.Default
:适合 CPU 密集型的任务,比如计算
总结:
协程在执行到有suspend标记的函数时,会被suspend,也就是被挂起,而所谓的被挂起,就是切个线程;挂起函数在执行完成之后,协程会重新切回它原先的线程
在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作
这个「切回来」的动作,在 Kotlin 里叫做resume恢复
suspend
关键字:
只是一个提醒,就是限制这个函数只能在协程里被调用,如果在非协程的代码中调用,就会编译不通过。
什么时候需要自定义 suspend 函数:
- 需要做耗时操作,I/O 操作和 CPU 计算工作。比如文件的读写、网络交互、图片的模糊处理
- 延迟处理操作
具体实现:
- 给函数加上
suspend
关键字,然后在withContext
把函数的内容包住就可以了 - 挂起函数
delay
,等待一段时间后再继续往下执行代码
5.非阻塞式挂起
三、总结
- 协程就是切线程;
- 挂起指的是可以自动切回来的切线程;
- 非阻塞式挂起指的是它能用看起来阻塞的代码写出非阻塞的操作