Kotlin 协程是一套轻量级并发编程方案,旨在简化异步操作和并发任务的实现,解决传统异步编程中回调地狱和线程管理复杂等问题。Kotlin 协程是运行在线程中的,可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。
要在 Android 中使用协程,首先需要引入依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation "androidx.activity:activity-ktx:1.5.1"
协程
启动协程
CoroutineScope(Dispatchers.IO).launch {
// I/O 线程
}
launch 是一个函数,用于创建协程并将其函数主体的执行分派给相应的调度程序,Dispatchers.IO 指示此协程应在 I/O 线程上执行。
withContext 函数可以切换到指定线程,并在闭包中逻辑执行完后自动把线程切回去继续执行。
CoroutineScope(Dispatchers.Main).launch {
// 主线程执行操作
withContext(Dispatchers.IO) {
//切换到 I/O 线程
}
//todo 主线程接着操作
}
如果一个函数任务比较耗时,我们可以把它写成 suspend 函数,suspend 用于暂停执行当前协程,并保存所有局部变量,在执行到某一个 suspend 函数时,这个协程就会被挂起。
举个例子,我们有一个耗时任务,这里用 delay 模拟耗时,它的作用是等一段时间后再继续往下执行代码。
private suspend fun executeTask() {
delay(3000)
}
CoroutineScope(Dispatchers.Main).launch {
executeTask()
Log.i(tag, "Task execution completed")
}
Log.i(tag, "Jump out")
当我们 launch 启动协程时,遇到了 suspend 耗时函数,这个协程就会被挂起,此时会跳出该协程,先打印出 Jump out,等挂起执行完之后又会重新切回,所以后面才会打印出 Task execution completed
Kotlin 提供了三个调度程序,以用于指定应在何处运行协程:
- Dispatchers.Default:主要用于 CPU 密集型任务的调度器,它会使用共享的后台线程池,当有一些复杂的计算任务,如大规模的数据处理、加密算法等,使用这个是不错的选择。
- Dispatchers.IO:主要用于执行 I/O 操作,如网络请求、文件读写等。
- Dispatchers.Main:主要用于在主线程中执行协程。
Dispatchers.Main 的内部实现依赖于 Android 的主线程 Looper 和 Handler 机制。它通过将协程任务包装成 Runnable 并投递到主线程的 Looper 中,实现了协程代码在主线程上的执行。
Dispatchers.IO 的内部实现基于线程池机制,通过动态调整线程数量和任务队列来高效执行任务。
除了 launch,我们还可以用 async 来启动一个协程。它俩的区别在于
- launch:启动新协程不将结果返回给调用方
- async:启动新协程并允许使用 await 的挂起函数返回结果
比如有两个协程,这两个并行的协程需要 getData 在返回结果之前完成,getData 返回两个任务的执行结果之和。
private suspend fun executeTask1(): Int {
delay(1000)
return 1
}
private suspend fun executeTask2(): Int {
delay(2000)
return 2
}
// coroutineScope 用于启动一个或多个协程
private suspend fun getData() = coroutineScope {
val task1 = async { executeTask1() }
val task2 = async { executeTask2() }
task1.await() + task2.await()
}
await 针对单个协程,awaitAll 针对多个协程。
CoroutineScope 会跟踪它使用 launch 或 async 创建的所有协程,我们可以随时调用 scope.cancel() 以取消正在运行的协程。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope。例如,ViewModel 有 viewModelScope,Lifecycle 有 lifecycleScope 。
Job 是协程的句柄,使用 launch 或 async 创建的每个协程都会返回一个 Job 实例,该实例是相应协程的唯一标识并管理其生命周期。
val job = lifecycleScope.launch {
delay(5000)
Log.i(tag, "finish")
}
findViewById<Button>(R.id.btn).setOnClickListener {
job.cancel()
}
在我们日常开发中,最常见的使用场景就是用协程去处理网络请求,现在 Retrofit 也原生支持协程了。
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
interface MyService {
@GET("gallery/{imageType}/response")
suspend fun getImages(@Path("imageType") imageType: String): List<String>
companion object {
fun createApi(): MyService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(MyService::class.java)
}
}
lifecycleScope.launch {
try {
val result = MyService.createApi().getImages("banner")
if (result.isNotEmpty()) {
refreshView(result) //刷新视图
}
} catch (e: Exception) {
e.printStackTrace()
}
}
由此可见,协程最方便的就是省去切线程的步骤,用同步代码处理耗时的异步网络请求,省去 Retrofit 中的网络回调。
协程中捕获异常除了 try-catch 外,还可以使用 CoroutineExceptionHandler。
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.i(tag, "Caught Exception: $throwable")
}
val scope = CoroutineScope(Job() + exceptionHandler)
scope.launch {
throw RuntimeException("Test exception")
}
协程的底层依赖线程池,但协程的挂起不会阻塞线程。当一个协程挂起时,其线程会被释放,供其他协程或任务使用。
为什么说“协程挂起不会阻塞线程”?
协程在挂起时主动通知调度器:“我现在需要等待,你可以用这个线程做其他事情”。线程不会像传统阻塞调用(如 Thread.sleep())那样傻等,而是立即执行其他任务,挂起函数内部通过异步回调实现非阻塞。
调度器如何管理线程?
以 Dispatchers.IO 为例:底层使用 Executors 创建的线程池,当一个协程挂起时,调度器的 Worker 线程会从任务队列中取出下一个协程任务执行。
在 kotlin 协程中,有个概念叫做通道 Channel,属于热流,实际上它就是个队列,是一个面向多协程之间数据传输的 BlockQueue,用于协程间通信。Channel 允许我们在不同的协程间传递数据,通俗地讲,就是不同的协程可以往同一个管道里面写入或者读取数据,它使用 send 和 receive 两个方法往管道里面写入和读取数据,这两个方法是非阻塞的挂起函数,所以必须在协程中使用。
val channel = Channel<Int>()
lifecycleScope.launch(Dispatchers.IO) {
for (i in 0..5) {
channel.send(i)
}
}
lifecycleScope.launch {
delay(1000)
for (i in channel) {
Log.i(tag, "channel: $i") //会打印出 0-6
}
}
lifecycleScope.launch(Dispatchers.IO) {
delay(2000)
channel.send(6)
//停止接受新元素
channel.close()
}
由此可见,我们在协程外定义 Channel, 多个协程就可以访问同一个 Channel,达到协程间通信的目的。
Flow
- 冷流:只有订阅者订阅时,才开始执行发射数据流,冷流和订阅者只能是一对一关系,当有多个不同的订阅者时,消息是重新发送的。
- 热流:无论有没有订阅者订阅,事件都会发生。当热流有多个订阅者时,热流和订阅者们是一对多的关系,可以与多个订阅者共享信息。
flow 是冷流,flow 中的代码直到被 collect 调用时才会执行。
val dataFlow = flowOf(1,2,3)
lifecycleScope.launch {
dataFlow.collect {
Log.i(tag, "dataFlow.collect: $it")
}
}
如果需要定时取消 flow 的执行,可使用 withTimeoutOrNull 添加超时即可,如下所示,只会打印出 1
val dataFlow = flow {
for (i in 1..3) {
delay(1000)
emit(i)
}
}
lifecycleScope.launch {
withTimeoutOrNull(2000) {
dataFlow.collect {
Log.i(tag, "dataFlow.collect: $it")
}
}
}
操作符
map 操作符将 flow 的输入转换为新的输出,比如将 Int 转换成 String
val dataFlow = flowOf(1, 2, 3, 4, 5)
lifecycleScope.launch {
dataFlow.map {
"NO.$it"
}.collect {
Log.i(tag, "collect: $it")
}
}
transform 操作符与 map 操作符相似但又不完全一样,map 是一对一的变换,而 transform 可以完全控制流的数据。
val dataFlow = flowOf(1, 2, 3, 4, 5)
lifecycleScope.launch {
dataFlow.transform {
if (it > 3) {
emit("NO.$it")
}
}.collect {
Log.i(tag, "collect: $it")
}
}
filter 过滤操作符
lifecycleScope.launch {
dataFlow.filter {
it > 3
}.collect {
Log.i(tag, "collect: $it")
}
}
flowOn 指定线程的切换
val dataFlow = flow {
emit(1)
}.flowOn(Dispatchers.IO)
lifecycleScope.launch {
dataFlow.flowOn(Dispatchers.Main).collect {
Log.i(tag, "collect: $it")
}
}
组合操作符 combine 可以连接两个不同的 flow
val dataFlow1 = flowOf(1, 2, 3)
val dateFlow2 = flowOf("a", "b", "c")
lifecycleScope.launch {
dataFlow1.combine(dateFlow2) { p1, p2 ->
"$p1 -> $p2"
}.collect {
Log.i(tag, "collect $it")
}
}
组合操作符 merge 用于将多个流合并
val dataFlow1 = flowOf(1, 2, 3)
val dateFlow2 = flowOf("a", "b", "c")
lifecycleScope.launch {
listOf(dataFlow1, dateFlow2).merge().collect {
Log.i(tag, "collect: $it")
}
}
StateFlow 和 SharedFlow
StateFlow 和 SharedFlow 是 Flow API,允许数据流以最优方式发出状态更新并向多个使用方发出值,属于热流。
这两者的区别在于,StateFlow 只有在值改变时才会返回,如果发生更新但值没有变化时,StateFlow 不会回调 collect 函数,而 SharedFlow 支持发出和收集重复值。
SharedFlow
class MainViewModel : ViewModel() {
val sharedFlow = MutableSharedFlow<Int>()
var count = 1
fun changeValue() {
viewModelScope.launch {
count++
sharedFlow.emit(count)
}
}
}
val mainViewModel by viewModels<MainViewModel>()
lifecycleScope.launch {
mainViewModel.sharedFlow.collect {
Log.i(tag, "sharedFlow.collect: $it")
}
}
findViewById<Button>(R.id.btn).setOnClickListener {
// 点击按钮改变值
mainViewModel.changeValue()
}
StateFlow
class MainViewModel : ViewModel() {
var count = 0
val stateFlowFlow = MutableStateFlow(0)
fun changeValue() {
viewModelScope.launch {
count++
stateFlowFlow.value = count
}
}
}
val mainViewModel by viewModels<MainViewModel>()
lifecycleScope.launch {
mainViewModel.stateFlowFlow.collect {
Log.i(tag, "stateFlowFlow.collect: $it")
}
}
findViewById<Button>(R.id.btn).setOnClickListener {
// 点击按钮改变值
mainViewModel.changeValue()
}
StateFlow 和 LiveData 具有相似之处,两者都是可观察的数据容器类,但两者的行为有所不同:
- StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。
- 当 Activity 进入 STOPPED 状态时,liveData.observe 会自动取消注册使用方,而从 StateFlow 或其他数据流收集数据的操作并不会自动停止。如需实现相同的行为,官方推荐使用 repeatOnLifecycle 来构建协程,当视图处于 STARTED 状态时会开始收集流,并且在 RESUMED 状态时保持收集,最终在视图进入 STOPPED 状态时结束收集过程。
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mainViewModel.stateFlowFlow.collect {
Log.i(tag, "stateFlowFlow.collect: $it")
}
}
}
并发问题
我们先看个例子,这里重复888次,每次开启一个协程。
val scope = CoroutineScope(Dispatchers.Default) //创建协程作用域,Default 和 IO 都支持并发
var count = 0
repeat(888) {
scope.launch {
count++
Log.i(tag, "currentThread: ${Thread.currentThread().name},count: $count")
}
}
然后看如下打印:

最后并没有打印出888,也运行在两个不同的线程中,所以,说明协程中同样存在并发问题,那要怎么去解决协程的并发问题呢?当然,你也可以用 Java 的方式去解决,比如 synchronized, Atomic 机制等,下面介绍两种协程的方式。
单线程模式
使用 Dispatchers.Unconfined,协程始终运行在单线程中。
val scope = CoroutineScope(Dispatchers.Unconfined)
var count = 0
repeat(888) {
scope.launch {
count++
Log.i(tag, "currentThread: ${Thread.currentThread().name},count: $count")
}
}
Mutex
使用 Mutex 可以确保在同一时间只有一个协程可以访问被锁定的代码块
val mutex = Mutex()
val scope = CoroutineScope(Dispatchers.Default)
var count = 0
repeat(888) {
scope.launch {
mutex.withLock {
count++
Log.i(tag, "currentThread: ${Thread.currentThread().name},count: $count")
}
}
}