Android Kotlin 协程入门与基础用法详解
前言
本文面向具有一定 Kotlin 基础和 Android 开发基础的读者。文章将从零开始讲解 Kotlin 协程的基本使用、项目应用及部分原理知识,涵盖 Kotlin 基础知识、组件及常用第三方框架的基础使用。
项目配置
创建以 Kotlin 为开发语言的工程项目 KotlinCoroutineDemo,在 project/build.gradle 中引用:
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
在 app/build.gradle 中引用相关依赖:
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程 Android 支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
主要版本号:
- Android Studio: 4.1.3
- Kotlin: 1.4.32
- kotlinx-coroutines-android: 1.4.3
- Retrofit: 2.9.0
- OkHttp: 4.9.0
- Coil: 1.2.0
- Room: 2.2.5
Kotlin 协程基础介绍
为了方便表述,下文将 Kotlin 协程简称为协程(Coroutine)。
什么是协程
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程上调度执行,而代码则保持如同顺序执行一样简单。
简单的概括就是我们可以以同步的方式去编写异步执行的代码。协程是依赖于线程的,但是协程挂起时不需要阻塞线程,几乎是无代价的。所以协程像是一种用户态的线程,非常轻量级,一个线程中可以创建 N 个协程。
协程的创建通过 CoroutineScope,启动方式有三种:
runBlocking<T>:启动一个新的协程并阻塞调用它的线程,直到里面的代码执行完毕,返回值是泛型T。launch<Job>:启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个 Job。async<Deferred<T>>:启动一个协程但不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用。以Deferred对象的形式返回协程任务。
Job、Deferred 与协程作用域
Job
Job 可以认为就是一个协程作业,是通过 CoroutineScope.launch 生成的。它运行一个指定的代码块,并在该代码块完成时完成。我们可以通过 isActive、isCompleted、isCancelled 来获取到 Job 的当前状态。
| State | [isActive] | [isCompleted] | [isCancelled] |
|---|---|---|---|
| New (optional initial state) | false | false | false |
| Active (default initial state) | true | false | false |
| Completing (transient state) | true | false | false |
| Cancelling (transient state) | false | false | true |
| Cancelled (final state) | false | true | true |
| Completed (final state) | false | true | false |
Deferred
Deferred 继承自 Job,可以把它看做一个带有返回值的 Job。
public interface Deferred<out T> : Job {
public suspend fun await(): T
public val onAwait: SelectClause1<T>
public fun getCompleted(): T
public fun getCompletionExceptionOrNull(): Throwable?
}
我们需要重点关注 await() 方法,可以通过 await() 方法获取执行流的返回值,当然如果出现异常或者被取消执行,则会抛出相对应的异常。
协程作用域
协程作用域(Coroutine Scope)是协程运行的作用范围。launch、async 都是 CoroutineScope 的方法。CoroutineScope 定义了新启动的协程作用范围,同时会继承它的 coroutineContext 自动传播其所有的 elements 和取消操作。换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。
Kotlin 协程的基础用法
运行第一个协程
在 Android 中有一个名为 GlobalScope 全局顶级协程,这个协程是在整个应用程序生命周期内运行的。我们就以此协程来使用 launch 和 async 启动。
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
class MainActivity : AppCompatActivity() {
private lateinit var btn: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn = findViewById(R.id.btn)
btn.setOnClickListener {
start()
}
}
private fun start() {
runBlocking {
Log.d("runBlocking", "启动一个协程")
}
GlobalScope.launch {
Log.d("launch", "启动一个协程")
}
GlobalScope.async {
Log.d("async", "启动一个协程")
}
}
}
运行 app,点击按钮执行 start() 方法,控制台上可以看到输出:
D/runBlocking: 启动一个协程
D/launch: 启动一个协程
D/async: 启动一个协程
runBlocking 的返回值
runBlockingJob 的输出结果默认返回的是该协程作业的当前状态。如果在 runBlocking 协程最后一行增加一个返回值:
val runBlockingJob = runBlocking {
Log.d("Coroutine", "runBlocking 启动一个协程")
"我是 runBlockingJob 协程的返回值"
}
将会看到如下输出:
D/Coroutine: runBlocking 启动一个协程
D/runBlockingJob: 我是 runBlockingJob 协程的返回值
runBlocking 的设计目的是将常规的阻塞代码连接在一起,主要用于 main 函数和测试中。
launch 函数
launch 方法最终返回的是一个 coroutine 对象。由于没有传入值,其最后返回的是一个 StandaloneCoroutine 对象。StandaloneCoroutine 继承自 AbstractCoroutine,而 AbstractCoroutine 实现了 Job 接口。
async 函数
async 返回的 DeferredCoroutine 对象。DeferredCoroutine 同时也继承 Deferred<T> 接口。那么问题来了,我们要怎么获取到这个 Deferred<T> 携带的返回值 T 呢?
我们在一开始的时候提到需要重点关注 Deferred 的 await() 方法,可以通过返回 Deferred 对象,调用 await() 方法来获取返回值。
挂起函数
suspend 是协程的关键字,表示这是一个挂起函数,每一个被 suspend 修饰的方法只能在 suspend 方法或者在协程中调用。
private fun start() {
GlobalScope.launch {
val launchJob = launch {
Log.d("launch", "启动一个协程")
}
Log.d("launchJob", "$launchJob")
val asyncJob = async {
Log.d("async", "启动一个协程")
"我是 async 返回值"
}
Log.d("asyncJob.await", ":${asyncJob.await()}")
Log.d("asyncJob", "$asyncJob")
}
}
现在我们通过 GlobalScope.launch 启动里一个协程,同时在协程体里面通过 launch 直接又启动了 2 个协程。因为通过 runBlocking、launch 和 async 启动的协程体等同于协程作用域,所以这里就可以直接使用 launch 启动一个协程。
Android 中的协程并发与同步
因为协程采用的是并发设计模式,所以导致 launch 和 async 的协程体内的 log 日志输出是无序方式。但是如果某个协程满足以下几点,那它里面的子协程将会是同步执行的:
- 父协程的协程调度器是处于
Dispatchers.Main情况下启动。 - 同时子协程在不修改协程调度器下的情况下启动。
private fun start() {
GlobalScope.launch(Dispatchers.Main) {
for (index in 1 until 10) {
// 同步执行
launch {
Log.d("launch$index", "启动一个协程")
}
}
}
}
输出日志显示为顺序执行:
D/launch1: 启动一个协程
D/launch2: 启动一个协程
...
D/launch9: 启动一个协程
这是因为在 Android 平台上如果协程处于 Dispatchers.Main 调度器,它会将协程调度到 UI 事件循环中执行,即通常在主线程上执行,这样就能理解为什么是同步执行了吧。如果是不同步的话,那在操作 UI 刷新的时候,就会出现各种问题。
总结与展望
至此,我们对 runBlocking、launch、async 的相关介绍就到这里。下一章节将对以下知识点做初步讲解:
- 协程调度器
CoroutineDispatcher - 协程上下文
CoroutineContext作用 - 协程启动模式
CoroutineStart - 协程作用域
CoroutineScope - 挂起函数以及
suspend关键字的作用


