Kotlin 语言发展至今,也有9个年头了。当初发明之后一直不愠不火,直到近年 Google 宣布将支持使用 Kotlin 语言支持 Android 开发之后,Kotlin 才算是真正火起来。

在 Android 开发中,Kotlin 语言与 Java 语言可以无缝衔接,其实背后还是 Android 虚拟机的功劳,它会将 Kotlin 文件转换为 class 文件,然后加载到虚拟机中运行。在本文写作时,Kotlin 版本已经到了 1.3.721.4.0 正处于 preview 阶段。关于 Kotlin 语言的学习,我会单开一篇文章来讲,这篇文章我们来讲讲 Kotlin 中一个非常重要的特性——协程(Coroutine)。

协程的概念

协程的概念由来已久,当初邓超代言广告的时候。。。喂喂喂,此协程非彼携程啊!

好,重新来。

协程的概念由来已久,它又被称为『微线程』、『纤程』。从名字上可以看出,它是一种非常轻量级的概念。它很早就被提出来了,进程下有线程,那线程下是不是也得分一分?好,就叫协程吧。虽然概念的提出很早,但也只是近几年才得到广泛应用。

进程下有多个线程

线程下有多个协程

我们可以给它下一个定义:协程是能够在单个线程下,还能够并发运行的一种机制。它最重要的特点是,它不会被操作系统内核所管理,而完全由用户程序来控制,也就是在用户态执行,而非内核态。这样带来的好处是可以省去传统多线程机制中线程切换时的性能损耗,大幅度提高并发性能。

在 Python 中很早就引入了协程的使用,我们可以简单看一下一个很简单的生产者-消费者的例子

# 消费者
def consume():
    while True:
        number = yield  # 在这停顿
        print('Start Consuming', number)

consumer = consume()

# 初始化协程,并在 yield 处停止
next(consumer)

# 生产者
for num in range(0, 100):
    print('Start Producing', num)
    consumer.send(num)

上面的代码创建了一个协程consumer,然后在主线程中生产数据,并交给协程去消费。yield是 Python 的语法,指的是程序执行到这儿的时候,会停顿,等待主线线使用send()发送消息时,才会接收消息并继续执行。

上面的代码运行如下:

Start Producing 0
Start Consuming 0
Start Producing 1
Start Consuming 1
Start Producing 2
Start Consuming 2
Start Producing 3
Start Consuming 3
...
...
Start Producing 97
Start Consuming 97
Start Producing 98
Start Consuming 98
Start Producing 99
Start Consuming 99

各位同学们看出来了没有,实际上这是一种『障眼法』,两段代码完全是在程序的控制下交替执行,虽然我们使用线程 + 同步机制也可以做到这一点,但是在线程切换的资源消耗以及性能上,就比这种做法要差上那么『一 nai nai』。

在 Python 3.5+ 上,使用async/await来替代了yield/send()

还有其他的语言也使用了协程的概念,比如 Lua、Go、C# 等等。

协程与线程的比较

上面我们提到了,协程是比线程还要低一级的并发机制,那么它们两个有什么相同点和不同之处呢?理论上来说,这两种概念不太应该用来比较,但是我们还是列举一下,也方便我们理解协程的优缺点。

线程

线程拥有独立的栈、局部变量、可以共享进程的内存。在使用共享数据时,为了避免造成数据错误,需要控制线程的状态。但这种控制是由内核来做的,所以程序会不断地在用户态和内核态之间切换,这种切换的消耗和代价是比较大的。

协程

协程也拥有自己的栈、局部变量,可以共享线程内的内存。在一个线程上可以同时跑多个协程,同一时间只有一个协程被执行,本质上是在单线程上模拟多线程并发。协程的运行和暂停,全部是由开发者自己决定的,不需要切换至内核态,大大减少了消耗,提高了性能。

协程默认是非阻塞式的(也可以阻塞),一个协程在进入阻塞后不会阻塞当前线程,当前线程会去执行其他协程任务。

Kotlin 中的协程

Kotlin 的协程的中心思想是:暂停计算。简单来说,就是一个方法在执行到某处的时候,可以暂停,然后在到达某一个时间节点时,继续运行。把这种概念应用到编程中,就是可以直接编写无阻塞的代码,能达到与阻塞代码相同的效果。官网给出了一个例子:

fun postItem(item: Item) {
    launch {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
}

suspend fun preparePost(): Token {
    // 执行请求,暂停协程
    return suspendCoroutine { /* ... */ } 
}

这块代码并不会阻塞主线程,preparePost()被称为『可中止方法』。与上面讲过的中心思想一致:方法被执行 → 暂停执行 → 在某个时间节点继续执行。

使用 Kotlin 的协程有下面几个优点:

  1. 方法本身不变,唯一的变化是加上了suspend关键字;
  2. 写代码时直接采用同步写法,看起来就是『由上至下』的执行方式,不用任何特殊的语法、特殊的处理。我们只需要使用launch关键字来启动协程并执行可中止方法即可;
  3. Kotlin 中其他的 API 等照常使用,你可以随便使用循环、异常处理之类的,不需要再学习什么新的 API;
  4. 平台无关性。无论你的代码是运行在 JVM 上,Javascipt 平台或者是其他平台,代码是一样一样的,让编译器来帮你做这些适配的工作。

Android 中的 Kotlin 协程

要在 Android 中使用 Kotlin 协程,要分为两步:

  1. 将 Kotlin 集成到项目中。

首先在项目的 build.gradle 文件中,修改成如下代码:

buildscript {
    ext.kotlin_version = '1.3.72'  // kotlin 的版本
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.3'  // 要使用 kotlin,gradle 版本必须在 3.0.0 以上
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"  

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
...

然后在模块的 build.gradle 文件中,修改代码:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    ...
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    ...
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    ...
}

同步 gradle,Kotlin 就集成完毕了。

  1. 加入协程的依赖

在模块的 buidl.gradle 文件中,做出如下修改:

dependencies {
    ...
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.3.72"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
}

就完成了协程的集成工作。

然后我们使用协程来写一个简单的倒计时的小例子,我们设计一个 textView 和一个 button,当点击 button 时,textView 就会显示 10 秒钟的倒计时,精确到毫秒。

button.setOnClickListener {
    GlobalScope.launch(Dispatchers.Main) {
        println(Looper.myLooper() == Looper.getMainLooper())

        for (i in 1000 downTo 0) {
            textView.text = formatTime(i)
            delay(1)
        }
    }
}

可以看出,与上面简单介绍的 suspend、launch 关键字不同,这里又多了一些新的东西。

  • GlobalScope

GlobalScope 用来启动顶级的协程,生命周期与 Application 一致。当然,还有其他的 Scope,我们回头慢慢介绍。

  • Dispatchers

用来指定协程运行在哪个线程里。毕竟网络请求还是要在子线程中,更新 UI 还是要在主线程哇。后面会讲一个简单的网络请求的例子。

Dispatcher 有几种类型,如下表所示:

常量 作用
Default
Main 让协程运行在主线程