快速上手 Kotlin 开发系列之什么是协程

199 阅读5分钟

站在巨人的肩膀上做个笔记,摘录自:kaixue.io/kotlin-coro…

协程是什么

协程的概念并没有官方的或者统一的定义,协程原本是一个跟线程非常类似的用于处理多任务的概念,是一种编程思想,并不局限于特定的语言。

那在 Kotlin 中的协程是什么呢?

其实就是一套有 Kotlin 官方提供的线程 API。就像 Java 的 Executor 和 Android 的 AsyncTask,Kotlin 协程也对 Thread 相关的 API 做了一套封装,让我们不用过多关心线程也可以方便地写出并发操作,这就是 Kotlin 的协程。

协程的好处

既然 Java 有了 Executor,Android 又有了 Handler 和 AsyncTask 来解决线程间通信,而且现在还有 RxJava,那用协程干嘛呢?协程好在哪儿呢?

协程的好处,本质上跟其他的线程 API 一样,都是为了方便使用,但由于它借助了 Kotlin 的语言优势所以比起其他的实现方案要更方便一点。

协程最基本的功能就是并发也就是多线程,用协程你可以把任务切到后台执行:

launch(Dispatchers.IO) {
  //耗时
}

切到前台:


launch(Dispatchers.Main) {
   //更新 UI
}

这种写法很简单,但它不能算是协程直接使用 Thread 的优势,因为 Kotlin 专门添加了一个函数来简化对线程的直接使用:

thread {
    ...
}

而 Kotlin 最大的好处是在于你可以把运行在不同线程的代码,写在同一个代码块里,上下两行代码,线程切走再切回来,这是在写 Java 时绝对做不到的。它可以用看起来同步的方式写出异步代码,这就是 Kotlin 最有名的 非阻塞式挂起。例如:

CoroutineScope(Dispatchers.Main).launch {
    val bitmap = suspendingGetBitmap()// 网络请求,后台线程
    imageView.setImageBitmap(bitmap)// 更新 UI 主线程
}

Java 中我们处理这样的场景需要使用回调来解决,一旦逻辑复杂很容易会出现两层或 n 层回调,这就陷入了回调地狱

而 Kotlin 的协程就完全消除了回调!另外大家不要忘了,回调式可不止多了几个缩进那么简单,它也限制了我们的能力。

比如,我们有个需求,他需要分别执行两次网络请求,然后把结果合并后再展示到界面,按照正常思路这两个接口应该同时发起请求,然后把结果做融合。

但是这种场景如果使用 Java 的回调式就会比较吃力,可能会把两个接口做串行的请求,但这很明显是垃圾代码!(暂时先不考虑 RxJava)

而使用协程可轻松实现,依然是上下两行代码:

launch(Dispatchers.Main) {
    val avatar = async { getAvatar() }//获取用户头像
    val logo = async { getLogo() }//获取 Logo
    mergeShowUI(avatar.await(), logo.await())//合并展示
}

所以由于 Kotlin 消除了并发任务之间协作的难度,协程可以让我们轻松的写出复杂的并发代码。这些就是协程优势所在!

async 会在后面的章节中介绍。

协程的基本使用

配置协程

要想在 Android 工程中使用 Kotlin 协程,需要配置相应的依赖:

根目录下的 build.gradle 配置版本:

buildscript {
    ...
    ext.kotlin_coroutines = '1.3.1'
    ...
}

app 下的 build.gradle:

dependencies {
    ...
    //                                        👇 依赖协程核心库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines"
    //                                        👇 依赖当前平台所对应的平台库
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines"
    ...
}

创建协程

上面提到的 launch 函数不是一个顶层函数,是不能直接使用的,可以通过下面的三种方法来创建:

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

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

// 方法三,自行通过 CoroutineContext 创建一个 CoroutineScope 对象
//                                    👇 需要一个类型为 CoroutineContext 的参数
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    getImage(imageId)
}
  • 方法一通常用于单元测试场景,业务开发一般不会用它,因为它是线程阻塞的;
  • 方法二和方法一的区别在于不会线程阻塞。但在 Android 中并不推荐这种用法,因为它的生命周期与 Application 一致,且不可取消;
  • 方法三为推荐方式,可以通过传入的 context 参数来管理协程的生命周期(注:这里的 context 和 Android 里的不是一个)

使用协程

接下来开始介绍协程的具体使用,最简单的方式上面已经介绍了:

launch(Dispatchers.IO) {
    ...
    getImage(imageId)
    ...
}

这个 launch 函数表示含义就是我要创建一个新的协程,并在指定的线程上运行它,这个被创建的协程是谁?就是你传给 launch 的那些代码:

...
getImage(imageId)
...

这段连续的代码就是协程。

所以我们就清楚了什么时候用协程,就是当你切线程或者指定线程的时候

例如,子线程中获取图片,主线程中更新 UI:

launch(Dispatchers.IO) {
    val image = getImage(imageId)
    launch(Dispatchers.Main) {
        iv.setImageBitmap(image)
    }
}

???什么情况,还是回调地狱???

如果只用 launch 函数协程并不比直接使用线程更加方便,但是协程里有一个很厉害的函数 withContext().

这个函数可以指定线程来执行代码,并且在执行完成之后 自动把线程切回来 继续执行。

launch(Dispatchers.Main) {
    val image = withContext(Dispatchers.IO){
        getImage(imageId)
    }
    iv.setImageBitmap(image)
}

这种写法跟刚才看起来区别不大。 但是如果有了更多的线程切换区别就体现出来了,由于有了自动切回来的功能,协程消除了并发代码在协作时的嵌套,直接写成了上下关系的代码就能让多线程之间进行协作,这就是协程。

以上就是本节内容,欢迎大家关注👇👇👇

长按关注