Kotlin 语法四 协程

协程

协程:轻量级线程,由语言+标准库调度,不是操作系统线程;开销远小于线程、支持挂起非阻塞。

下面代码需要在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()
}

原理:

  1. 先启动 hello1 协程,执行到 delay → hello1 挂起,主线程让出
  2. 父协程继续执行 hello,执行到 delay → hello 挂起
  3. 定时到了之后,调度器在主线程恢复对应的协程继续往下跑

这是【单线程协作并发】,靠挂起点主动让出线程,不是线程切换、不是操作系统抢占调度。

❌ 反例(顺序执行,不会交替):

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. 易错点

  1. Flow冷流:没有collect,flow{}里代码完全不执行
  2. flowOn只向上生效;下游collect在调用协程所在上下文
  3. StateFlow更新相同value不会触发collect
  4. SharedFlow默认replay=0,晚订阅会丢失之前事件
  5. 结构化并发:collect所在协程取消,流自动停止

9. 操作符补充

  • onStart:收集开始前执行
  • onCompletion:流正常结束/取消后执行
scss 复制代码
simpleFlow()
    .onStart { println("开始收集") }
    .onCompletion { println("流结束") }
    .collect { println(it) }
相关推荐
stevenzqzq1 小时前
android 维度和渠道
android
xiaogai_gai2 小时前
生产汇报单查询同步实战:从金蝶云星空拉取到轻易云数据集成平台的工程化做法
android
释厄6233 小时前
错误无误公理——错误正确本同源都迭代定律——逆转有价值
android·开发语言·kotlin
wno7043 小时前
MYSQL 常用合并函数
android·数据库·mysql
事圆则缓4 小时前
HTTP 请求是怎么走的:从 Android 页面到服务端响应
android·网络协议·http
美狐美颜SDK开放平台6 小时前
直播APP平台搭建需要哪些技术?视频美颜SDK只是其中一环
android·ios·音视频·直播美颜sdk·视频美颜sdk
AIsoft_86896 小时前
传统记录方式为什么越来越难满足需求?
android·iphone
小π军7 小时前
安卓、iOS、鸿蒙三端的理解
android·ios·harmonyos
BD_Marathon7 小时前
ChatPromptTemplate的高级特性
android·java·javascript