协程
协程:轻量级线程,由语言+标准库调度,不是操作系统线程;开销远小于线程、支持挂起非阻塞。
下面代码需要在android环境运行,不能纯kotlin环境。
kotlin
// 关键现象:TEST_FINISH → 协程1 start → 协程2 start → TEST_OUTSIDE
// 根因:Main.immediate 嵌套协程触发局部微型事件循环,优先排空本地immediate队列,再返回外层同步代码
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
launch {
// 顺序2
Log.d("TEST_COROUTINE", "协程1 start")
delay(5000) // ✅协程非阻塞延时,代替Thread.sleep
// 顺序4
Log.d("TEST_COROUTINE", "协程1 finish")
}
launch{
// 顺序3
Log.d("TEST_COROUTINE", "协程2 start")
delay(5000)
// 顺序5
Log.d("TEST_COROUTINE", "协程2 finish")
}
// 顺序1
println("TEST_FINISH")
}
println("TEST_OUTSIDE")
}
}
lifecycleScope 的基础上下文:SupervisorJob() + Dispatchers.Main.immediate。
协程构建器 launch 本身没有固定默认调度器 ,子协程默认继承父协程的 coroutineContext。
在 Main.immediate 协程内部再嵌套launch启动子协程(继承得到 Main.immediate)时,会触发嵌套事件循环 : 父协程代码执行完之后、父 launch 返回之前,会先消费、执行排队的 immediate 子协程(直到遇到 suspend 挂起点),然后才回到外层同步代码执行 TEST_OUTSIDE。
协程体内可以直接使用 coroutineContext 拿到当前协程上下文,可以取出调度器:
kotlin
lifecycleScope.launch {
val ctx: CoroutineContext = coroutineContext
// 取出调度器 Dispatcher
val dispatcher = ctx[ContinuationInterceptor] as? CoroutineDispatcher
// TEST 当前dispatcher: Dispatchers.Main.immediate
println("TEST 当前dispatcher: $dispatcher")
}
👉 在当前这个父协程上下文为 Dispatchers.Main.immediate 的前提下:
scss
launch {
println("TEST_COROUTINE , 协程1 start ${Thread.currentThread().name}")
delay(5000)
println("TEST_COROUTINE, 协程1 finish")
}
等价于
scss
launch(Dispatchers.Main.immediate) {
println("TEST_COROUTINE , 协程1 start ${Thread.currentThread().name}")
delay(5000)
println("TEST_COROUTINE, 协程1 finish")
}
⚠️ 重要:等价关系依赖父上下文,如果父协程调度器不是 Main.immediate,则不再等价。
coroutineContext可以直接拿到当前协程的上下文。可以看到当前的上下文是Dispatchers.Main.immediate。因此里面的launch方法的默认也是
kotlin
lifecycleScope.launch {
val ctx: CoroutineContext = coroutineContext
// 取出调度器 Dispatcher
val dispatcher = ctx[ContinuationInterceptor] as? CoroutineDispatcher
// TEST 当前dispatcher: Dispatchers.Main.immediate
println("TEST 当前dispatcher: $dispatcher")
launch的时候,子协程默认继承父上下文。这里不写就是默认的父上下文的Dispatchers.Main.immediate。
scss
launch() {
// 顺序2
println("TEST_COROUTINE , 协程1 start ${Thread.currentThread().name}")
delay(5000) // ✅协程非阻塞延时,代替Thread.sleep
// 顺序4
println("TEST_COROUTINE, 协程1 finish")
}
等价于
scss
launch(Dispatchers.Main.immediate) {
// 顺序2
println("TEST_COROUTINE , 协程1 start ${Thread.currentThread().name}")
delay(5000) // ✅协程非阻塞延时,代替Thread.sleep
// 顺序4
println("TEST_COROUTINE, 协程1 finish")
}
Dispatchers.Main
kotlin
// 如果不想使用默认的上下文,改成Dispatchers.Main。
// 关键现象:TEST_FINISH → TEST_OUTSIDE → 协程1 start → 协程2 start
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
launch(Dispatchers.Main) {
// 顺序2
println("TEST_COROUTINE , 协程1 start ${Thread.currentThread().name}")
delay(5000) // ✅协程非阻塞延时,代替Thread.sleep
// 顺序4
println("TEST_COROUTINE, 协程1 finish")
}
launch(Dispatchers.Main){
// 顺序3
println("TEST_COROUTINE, 协程2 start ${Thread.currentThread().name}")
delay(5000)
// 顺序5
println("TEST_COROUTINE 协程2 finish")
}
// 顺序1
println("TEST_FINISH")
}
println("TEST outside of lifecycleScope")
}
}
suspend
suspend 用来标记一个具备挂起、恢复能力 的函数。 如果 suspend 函数内部执行到 delay 这类可取消挂起点,当前协程挂起,释放当前线程,线程可以调度执行其他就绪的协程。
⚠️ suspend函数调用限制:只能在 另一个suspend函数内,或者 launch/async 这类协程构建器的lambda(协程体)里面调用。
suspend 函数本身不会自动新开协程、不会自动并发;想要并发,必须包一层 launch/async,生成独立Job。
kotlin
suspend fun hello(){
println("TEST suspend begin")
for (i in 0..100){
delay(100)
println("TEST suspend at index $i")
}
println("TEST suspend finish")
}
suspend fun hello1(){
println("TEST suspend1 begin")
for (i in 0..100){
delay(100)
println("TEST suspend1 at index $i")
}
println("TEST suspend1 finish")
}
下面写法,hello 和 hello1 是两个独立协程,会交替执行;都运行在 main 主线程:
scss
lifecycleScope.launch {
launch {
hello1()
}
hello()
}
原理:
- 先启动 hello1 协程,执行到 delay → hello1 挂起,主线程让出
- 父协程继续执行 hello,执行到 delay → hello 挂起
- 定时到了之后,调度器在主线程恢复对应的协程继续往下跑
这是【单线程协作并发】,靠挂起点主动让出线程,不是线程切换、不是操作系统抢占调度。
❌ 反例(顺序执行,不会交替):
scss
lifecycleScope.launch {
hello()
hello1()
}
// hello全部执行完毕,才会执行hello1,两个跑在同一个协程里面
runBlocking
runBlocking:阻塞当前调用它的线程,直到 runBlocking 内部的协程全部执行完成,才会解除阻塞、继续执行后面的同步代码。
runBlocking 的lambda内部是 suspend 上下文,可以调用suspend函数(delay等)。
内部协程遇到delay会挂起协程,但 runBlocking 会hold住外层线程等待,不会让线程去干别的。
scss
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
lifecycleScope.launch {
for (i in 1 .. 10){
println("TEST lifecycleScope at index $i")
delay(1000)
}
}
println("TEST begin")
runBlocking {
for (i in 0..10){
delay(1000)
println("TEST $i")
}
}
println("TEST finish")
}
}
上述代码,一旦开始执行runBlocking的代码,上面的lifecycleScope.launch的代码也不会执行。
coroutineScope(suspend 函数,结构化并发)
不阻塞线程,但是会挂起当前协程,等内部所有子协程全部结束,才恢复往下走
scss
suspend fun test() = coroutineScope {
launch { delay(1000); println("task1") }
launch { delay(2000); println("task2") }
}
async /await
async 是协程构建器,和 launch 同级;返回 Deferred<T>(继承Job),既可以控制协程生命周期(cancel/join),还可以用 await() 等待协程完成并拿到返回值。 launch 返回 Job,只能控制生命周期,不能获取返回结果。
kotlin
lifecycleScope.launch {
var a = async {
delay(1000)
100
}
var b = async {
delay(1000 * 5)
200
}
var c = async {
for (i in 0..10){
delay(1000)
println("TEST async c at $i")
}
}
launch {
delay(5000)
// 拿到Deferred句柄,可以取消、等待
// c.cancelAndJoin() 等价下面两行:先发取消信号,再等待取消完成
c.cancel()
c.join()
}
// await 是suspend函数:挂起当前协程,等待async任务完成,拿到返回值
var res = a.await() + b.await()
println("TEST res is $res")
}
调度器 Dispatchers(指定协程跑在哪个线程池)
Dispatchers.Default:CPU 密集型任务,线程池,默认Dispatchers.IO:IO 密集(网络、文件、数据库),可扩容线程池Dispatchers.Main:主线程(Android 才有,纯 JVM 控制台没有 Main)Dispatchers.Unconfined:不绑定线程,在哪挂起恢复在哪跑,极少用
kotlin
fun main() = runBlocking {
launch(Dispatchers.IO) {
println("IO协程:${Thread.currentThread().name}")
}
}
withContent
不创建新协程,复用当前协程,只是临时换调度器跑这一段代码;执行完自动切回原来的调度器,是「同协程内上下文切换」。
代码会放到目标调度器的线程池去执行;但是它是串行阻塞当前协程的(挂起等待),不会并发 。withContext 内部代码执行完毕之前,当前协程是挂起的,后面代码不会走。
scss
lifecycleScope.launch(Dispatchers.Main) {
println("【主线程】开始")
val result = withContext(Dispatchers.IO) {
println("【IO线程】执行IO")
delay(1000)
"数据"
}
// withContext跑完,自动切回 Main
println("【主线程】拿到$result,更新UI")
}
Kotlin Flow 数据流(冷流、SharedFlow、StateFlow)
Flow 是协程上的响应式数据流,冷流(Cold Stream) :没有收集(collect)的时候,上游不会执行;每次collect都会重新走一遍上游。 依赖:
scss
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-flow:1.9.0")
1. 基础 Flow 创建 & collect
只有执行collect的时候,flow里面的代码才会执行。
scss
fun flow1(): Flow<Int> = flow {
// flow{} 构建器,里面可以写suspend代码
emit(1) // 发射数据
delay(500)
emit(2)
delay(500)
emit(3)
}
lifecycleScope.launch {
flow1().collect {
println("TEST collect $it")
}
}
✅ 核心特点(冷流):
scss
val f = simpleFlow()
println("第一次收集")
f.collect { println(it) }
println("第二次收集")
f.collect { println(it) } // 上游重新执行一遍,重新emit 1,2,3
对比:launch/async 是一次性任务;Flow 是持续发射一系列数据的流。
2. Flow 常用中间算子(和集合算子很像,但是惰性)
中间算子:map、filter、transform、take、distinctUntilChanged,不触发执行 ,只是组装流水线 终端算子:collect、single、first、toList、reduce,触发流运行
scss
lifecycleScope.launch {
(1..10).asFlow()
.filter { it % 2 == 0 }
.map { it * it }
.collect { println("TEST it $it") }
}
transform:比map灵活,可以emit多个值
scss
lifecycleScope.launch {
(1..3).asFlow()
.transform {
emit(it)
emit(it * 10)
}
/*
* TEST it 1
* TEST it 10
* TEST it 2
* TEST it 20
* TEST it 3
* TEST it 30
* */
.collect { println("TEST it $it") }
}
take(n):只取前n个,然后取消流
scss
lifecycleScope.launch {
/*
* TEST it 0
* TEST it 1
* TEST it 2
* */
flow {
repeat(100) { emit(it) }
}.take(3).collect { println("TEST it $it") }
}
3. 线程切换 flowOn(重点!)
⚠️ flowOn 只影响上游,不影响下游
scss
fun flow1(): Flow<Int> = flow{
// TEST DefaultDispatcher-worker-1
println("TEST ${Thread.currentThread().name}")
emit(100)
delay(1000)
emit(200)
}
scss
lifecycleScope.launch {
flow1().flowOn(Dispatchers.IO).collect {
/*
* TEST collect 100 main
* TEST collect 200 main
* */
println("TEST collect ${it} ${Thread.currentThread().name}")
}
}
注意:不要在map/filter里面随便用withContext切换线程,规范上游用flowOn
4. 异常捕获 catch
kotlin
fun flow1(): Flow<Int> = flow{
emit(100)
throw RuntimeException("流出错了")
}
scss
lifecycleScope.launch {
flow1().catch { e ->
println("TEST 捕获异常:${e.message}")
emit(-1) // 可以兜底发射默认值
}
.collect { println("TEST it ${it}") }
}
catch只能捕获上游抛出的异常;collect里面的异常catch抓不到。
5. 热流 SharedFlow
SharedFlow:热流(Hot Stream) ,不管有没有collect,上游都会跑;一对多,多个收集者共享同一份数据流。
scss
lifecycleScope.launch {
val sharedFlow = MutableSharedFlow<Int>()
// 启动生产者
launch {
repeat(5) {
delay(500)
sharedFlow.emit(it)
}
}
launch {
sharedFlow.collect { println("TEST A收到 $it") }
}
launch {
delay(2000)
sharedFlow.collect { println("TEST B收到 $it") }
}
}
参数:
ini
MutableSharedFlow(
replay = 2, // 给新订阅者回放最近2条历史
extraBufferCapacity = 0, // 缓冲
onBufferOverflow = BufferOverflow.SUSPEND
)
6. StateFlow(Android最常用,SharedFlow的特化)
- StateFlow = 带唯一当前值的SharedFlow
- 永远有值,新订阅者立刻拿到最新的值
- 只在值不一样的时候才分发(类似 LiveData)
kotlin
fun main() = runBlocking {
val stateFlow = MutableStateFlow(0) // 初始值0
launch {
repeat(4) {
delay(300)
stateFlow.value = it
}
}
stateFlow.collect {
println("state: $it")
}
}
只读版本:
StateFlow<T>,对外暴露,防止外部修改
kotlin
private val _uiState = MutableStateFlow("加载中")
val uiState: StateFlow<String> = _uiState
7. 三者对比(面试高频)
| 类型 | 冷热 | 特点 | 适用场景 |
|---|---|---|---|
| Flow | 冷流 | 每次collect重跑上游,一对一 | 一次性查询、文件读取、顺序数据流 |
| SharedFlow | 热流 | 多订阅、可回放、无强制初始值 | 事件通知(点击、弹窗、消息) |
| StateFlow | 热流 | 继承SharedFlow,持有当前状态,自动去重 | UI状态:页面数据、加载状态 |
8. 易错点
- Flow冷流:没有collect,flow{}里代码完全不执行
- flowOn只向上生效;下游collect在调用协程所在上下文
- StateFlow更新相同value不会触发collect
- SharedFlow默认replay=0,晚订阅会丢失之前事件
- 结构化并发:collect所在协程取消,流自动停止
9. 操作符补充
onStart:收集开始前执行onCompletion:流正常结束/取消后执行
scss
simpleFlow()
.onStart { println("开始收集") }
.onCompletion { println("流结束") }
.collect { println(it) }