协程是什么?
官方定义:协程是一种并发设计模式,我们可以在 Android 平台上使用它来简化异步执行的代码。
官方说协程是一种设计模式是因为协程不是Kotlin独有的,Go和Python也有协程,它是一种设计思想。而Kotlin的协程就是一套Kotlin官方提供的线程API,它类似于Java的Executors和Android的AsyncTask,实际上它就是对Thread API的一套封装。
// Thread
Thread({
...
}).start()
// Executor
val executor = Executors.newCachedThreadPool()
executor.execute({
...
})
// 协程
launch({
...
})
那么Java有Executors,Android又增加了Handler和AsyncTask,而且我们还有RxJava这种开发神器,为什么我们还要学习协程呢?
协程的优势
- Kotlin语言优势,比起基于JAVA的方案更方便,更简洁
- 非阻塞式挂起,它可以用看起来同步的方式写出异步代码
launch(Dispatchers.Main) { // 在主线程开启协程
val user = api.getUser() // IO 线程执行网络请求
nameTv.text = user.name // 主线程更新 UI
}
而以前Java的写法是这样的:
api.getUser(new Callback<User>() {
@Override
public void success(User user) {
runOnUiThread(new Runnable() {
@Override
public void run() {
nameTv.setText(user.name);
}
})
}
@Override
public void failure(Exception e) {
...
}
});
这种回调式的写法,读起来相当难受。
可能我们习惯了这种写法会觉得还好,但是需要发起多个网络请求后再统一更新UI的情况下,我们可能会这样写:
api.getAvatar(user) { avatar ->
api.getCompanyLogo(user) { logo ->
show(merge(avatar, logo))
}
}
以上是垃圾代码,没有同时请求,浪费了性能,于是考虑这样写:
api.getAvatar(user, callback)
api.getCompanyLogo(user, callback)
然后可以在callback中通过responseCount变量做判断是否都再合并数据,最后更新UI。这样的方式很不优雅!
Kotlin协程中实现的话会是这样的:
coroutineScope.launch(Dispatchers.Main) {
// async 函数之后再讲
val avatar = async { api.getAvatar(user) } // 获取用户头像
val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo
val merged = suspendingMerge(avatar, logo) // 合并结果
//
show(merged) // 更新 UI
}
基本使用
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}
// 方法一,使用 runBlocking 顶层函数
runBlocking {
getImage(imageId)
}
// 方法二,使用 GlobalScope 单例对象
// 可以直接调用 launch 开启协程
GlobalScope.launch {
getImage(imageId)
}
// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
// 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
getImage(imageId)
}
- 方法一是线程阻塞的,适用于单元测试,业务开发中不常用
- 方法二不阻塞线程,但是生命周期和App一致,且不能取消,所以Android开发中也不推荐使用
- 方法三不阻塞线程,且生命周期可控,因此Android开发中推荐使用这种方式
协程最简单的使用就是切线程,切换到IO线程:
launch(Dispatchers.IO) {
...
}
切换到主线程:
launch(Dispatchers.Main) {
...
}
我们来看看发起一个简单的网络请求并更新UI的需求在协程中的实现
coroutineScope.launch(Dispatchers.IO) {
val image = getImage(imageId)
launch(Dispatchers.Main) {
avatarIv.setImageBitmap(image)
}
}
这种方式比回调更优雅,但是这种嵌套也同样会影响代码的可阅读性。
如果只是使用 launch 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数: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 线程
}
//
suspend fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
...
}
这就是之前说的「用同步的方式写异步的代码」了。
fun getImage(imageId: Int) = withContext(Dispatchers.IO) {
// IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion
}
意思是说,withContext 是一个 suspend 函数,它需要在协程或者是另一个 suspend 函数中调用。
suspend
suspend 是 Kotlin 协程最核心的关键字,它的中文意思是「暂停」或者「可挂起」。如果你去看一些技术博客或官方文档的时候,大概可以了解到:「代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。
「挂起」的本质
协程中「挂起」的对象是协程。
启动一个协程可以使用 launch函数,协程其实就是这个函数中闭包的代码块。
「挂起」就是这个协程从正在执行它的线程上脱离,在其他线程继续执行去了。
怎么「挂起」的?
suspend fun suspendingPrint() {
println("Thread: ${Thread.currentThread().name}")
}
I/System.out: Thread: main
没有切线程,输出的结果还是在主线程,因为它不知道往哪切,需要我们告诉它。
suspend fun suspendingGetImage(id: String) = withContext(Dispatchers.IO) {
...
}
挂起,就是一个稍后会被自动切回来的线程调度操作
suspend 的意义
这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?
它其实是一个提醒。
如果你的某个函数比较耗时,也就是要等的操作,那就把它写成 suspend 函数
给函数加上 suspend 关键字,然后在 withContext 把函数的内容包住就可以了
withContext 可以把线程自动切走和切回。
网友评论