Q1:结构化并发是什么?核心价值?
核心答案
协程之间形成父子层级关系,遵循三大规则:
- 父协程取消 → 所有子协程递归取消;
- 子协程异常 → 父协程及兄弟协程全部取消(除非使用 SupervisorJob);
- 父协程等待所有子协程完成。
价值:杜绝内存泄漏、避免异常丢失、简化管理。
流程图
精简源码
kotlin
scope.launch {
launch { /* 子协程1 */ }
async { /* 子协程2 */ }
}
// 父协程取消,两个子协程自动取消
Q2:什么是 Kotlin 协程?和线程的核心区别?
核心答案
协程是用户态轻量级执行单元 ,由 Kotlin 编译器+运行时管理,不依赖操作系统内核 ;线程是内核态调度,切换成本高;协程挂起不阻塞线程 ,千万协程可稳定运行。核心区别:调度层级、资源开销、阻塞/挂起模型。
流程图
高开销
阻塞线程] C[协程] --> D[用户态调度
极低开销
挂起不阻塞]
精简源码
kotlin
GlobalScope.launch {
delay(1000) // 挂起,不阻塞线程
println("协程")
}
Thread {
Thread.sleep(1000) // 阻塞线程
println("线程")
}.start()
Q3:协程的挂起与恢复原理是什么?
核心答案
挂起函数通过CPS 变换(Continuation Passing Style) ,将执行状态(局部变量、指令位置)保存到 Continuation;挂起时切出线程,恢复时从状态点继续执行,无线程阻塞 。核心:状态保存 + 线程切出 + 状态恢复。
流程图
精简源码
kotlin
suspend fun fetchData() {
val data = withContext(Dispatchers.IO) { "网络数据" }
println(data)
}
Q4:CoroutineScope 作用?为什么必须用?
核心答案
统一管理协程生命周期 ,实现结构化并发 ;作用:自动取消子协程、防止内存泄漏、统一异常管理 。禁止使用 GlobalScope(无生命周期,内存泄漏风险)。
流程图
精简源码
kotlin
class MyViewModel : ViewModel() {
fun load() = viewModelScope.launch { } // ViewModel销毁自动取消
}
// 禁止:GlobalScope.launch { }
Q5:多个挂起函数顺序调用时,状态机(label)如何工作?挂起和恢复的具体流程是什么?
核心答案
挂起函数被编译为状态机 。每当调用一个子挂起函数前,父协程的 label 会递增,然后调用子函数。如果子函数挂起(返回 COROUTINE_SUSPENDED),父函数也立即挂起,保留当前 label。当子函数恢复时,会回调父函数的 continuation.resumeWith(),重新进入父函数的状态机,根据 label 跳转到下一个挂起点之后继续执行。
流程图
精简源码
kotlin
suspend fun taskA() {
println("TaskA start")
delay(500) // 挂起点
println("TaskA end")
}
suspend fun taskB() {
println("TaskB start")
delay(300) // 挂起点
println("TaskB end")
}
suspend fun runTasks() {
println("Before taskA")
taskA() // 调用挂起函数 A
println("After taskA, before taskB")
taskB() // 调用挂起函数 B
println("After taskB")
}
// 执行顺序和打印结果(时间顺序):
// Before taskA
// TaskA start
// (等待 500ms)
// TaskA end
// After taskA, before taskB
// TaskB start
// (等待 300ms)
// TaskB end
// After taskB
编译后状态机伪代码(runTasks):
kotlin
fun runTasks(completion: Continuation<Unit>): Any? {
val sm = completion as? StateMachine ?: StateMachine(completion)
when (sm.label) {
0 -> {
println("Before taskA")
sm.label = 1
val result = taskA(sm)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
1 -> {
println("After taskA, before taskB")
sm.label = 2
val result = taskB(sm)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
println("After taskB")
return Unit
}
}
return Unit
}
Q6:Job 生命周期状态?
核心答案
四大状态:New → Active → Completing/Cancelling → Completed/Cancelled
- 调用
cancel()→ 进入 Cancelling 最终 Cancelled; - 执行完毕 → Completed;
- 协程取消是协作式,必须检查取消状态。
流程图
精简源码
kotlin
val job = scope.launch {
while (isActive) { /* 任务 */ }
}
job.cancel()
Q7:协程取消是协作式,什么意思?如何保证取消?
核心答案
协程不会强制终止,必须主动检查取消状态才能停止;保证方式:
- 使用
isActive判断; - 使用挂起函数(
delay/withContext自动响应取消); - 循环任务中调用
yield()/ensureActive()。
流程图
精简源码
kotlin
launch {
for (i in 0..100) {
ensureActive()
println(i)
}
}
Q8:SupervisorJob 作用?和普通 Job 区别?
核心答案
监督式 Job :子协程异常不影响父协程和其他子协程;普通 Job:子协程异常 → 传播到父 → 所有协程取消。适用:独立任务(如多个独立接口请求)。
流程图
精简源码
kotlin
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch { /* 崩溃不影响兄弟 */ }
scope.launch { /* 正常执行 */ }
Q9:协程异常传播机制?
核心答案
- 根协程(launch):异常直接抛出崩溃;
- async 根协程 :异常在
await()时抛出; - 子协程:异常向上传播,取消整个父子树;
- SupervisorJob:异常仅在自身协程内,不传播。
流程图
精简源码
kotlin
scope.launch { error("崩溃") }
scope.launch(SupervisorJob()) { error("仅自身崩溃") }
Q10:CoroutineExceptionHandler 使用场景与限制?
核心答案
全局异常捕获,处理未捕获的协程异常;限制:
- 只能用在根协程;
- 不能捕获
async根协程异常; - 不能捕获
try-catch已处理的异常。
流程图
精简源码
kotlin
val handler = CoroutineExceptionHandler { _, e -> log(e) }
scope.launch(handler) { error("被捕获") }
Q11:协程中 try-catch 最佳实践?
核心答案
- 子协程异常:必须在子协程内部 catch,外部无法捕获;
- async 异常:在
await()处 catch; - 独立任务:用
SupervisorJob+ 内部 try-catch。
流程图
精简源码
kotlin
scope.launch {
launch {
try { error("") } catch (e: Exception) { }
}
}
Q12:launch 和 async 区别?
核心答案
launch:无返回值,启动任务,返回Job;async:有返回值,返回Deferred,必须用await()获取结果;- 并发执行用
async,顺序执行用launch。
流程图
Job] C[async] --> D[有返回值
Deferred+await]
精简源码
kotlin
scope.launch {
val d1 = async { api.getData1() }
val d2 = async { api.getData2() }
val res = d1.await() + d2.await()
}
Q13:withContext 作用?为什么优先用它?
核心答案
指定调度器执行代码块 ,执行完毕自动切回原线程;比 async{}.await() 更轻量,无额外开销,是官方推荐线程切换方案。
流程图
切换调度器] B --> C[执行任务] C --> D[自动切回原线程]
精简源码
kotlin
suspend fun getData() = withContext(Dispatchers.IO) { api.getData() }
Q14:Android 四大调度器区别与使用场景?
核心答案
- Main:主线程,UI操作;
- IO:IO密集型(网络、数据库),线程池复用;
- Default:CPU密集型(计算、解析),核心数线程;
- Unconfined :不绑定线程,恢复线程不确定,禁止业务使用。
流程图
精简源码
kotlin
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) { api.getData() }
binding.tv.text = data
}
Q15:Android 协程内存泄漏场景及解决方案?
核心答案
泄漏场景:使用 GlobalScope;自定义 Scope 未绑定生命周期;协程持有 View/Activity 引用,页面销毁未取消。
方案:使用 lifecycleScope/viewModelScope;自定义 Scope 在销毁时 cancel()。
流程图
精简源码
kotlin
class MyFragment : Fragment() {
override fun onViewCreated() {
viewLifecycleOwner.lifecycleScope.launch { }
}
}
Q16:viewModelScope / lifecycleScope 原理?
核心答案
官方扩展 Scope,自动绑定生命周期:
viewModelScope:ViewModel 销毁时cancel();lifecycleScope:生命周期 DESTROYED 时cancel();
底层:CoroutineScope+SupervisorJob()+ 生命周期监听。
流程图
精简源码
kotlin
viewModelScope.launch { }
Q17:StateFlow 和 SharedFlow 区别?
核心答案
- StateFlow :有初始值 ,粘性,只保留最新值,用于UI状态;
- SharedFlow :无初始值,可配置重放缓存,用于事件(点击、Toast);
- 都是热流,观察者越多越高效。
流程图
粘性
状态管理] C[SharedFlow] --> D[无初始值
可配置重放
事件管理]
精简源码
kotlin
val uiState = MutableStateFlow(UiState.Loading)
val event = MutableSharedFlow<Event>()
Q18:Flow 背压处理方式?
核心答案
背压:生产者速度 > 消费者速度;处理方式:
buffer():缓存队列;conflate():只保留最新值;collectLatest:取消旧收集,只处理最新;flowOn:指定调度器隔离。
流程图
精简源码
kotlin
flow { emit(1) }.conflate().collect { }
Q19:Flow 的 catch 操作符与 try-catch 在收集处的区别?如何恢复异常后的发射?
核心答案
catch操作符:捕获上游 (在catch之前)的异常,可以重新发射替代值、重试或抛出;try-catch包裹collect:捕获下游 的异常(收集函数内),无法处理上游的发射异常。
恢复异常后的发射:catch中可以emit回退值,然后正常结束。
流程图
精简源码
kotlin
flow { emit(1); throw IOException() }
.catch { e -> emit(-1) }
.collect { println(it) } // 1, -1
Q20:flatMapConcat、flatMapMerge 与 flatMapLatest 的区别?
核心答案
flatMapConcat:串行执行,前一个内层 flow 完全结束后才收集下一个;flatMapMerge:并发执行,可指定并发数;flatMapLatest:只关心最新 ,新元素到达时取消上一个内层 flow。
适用场景:顺序处理、多个独立异步任务、搜索联想词。
流程图
精简源码
kotlin
flowOf(1,2,3).flatMapConcat { flow { emit(it*10); delay(100) } }.collect()
flowOf(1,2,3).flatMapMerge(2) { flow { emit(it*10); delay(100) } }.collect()
flowOf(1,2,3).flatMapLatest { flow { emit(it*10); delay(200) } }.collect()
Q21:suspend 关键字作用?没有它会怎样?
核心答案
- 标记函数为挂起函数,只能在协程/其他挂起函数中调用;
- 触发编译器生成状态机代码,实现挂起恢复;
- 没有 suspend,无法挂起,只能同步阻塞。
流程图
精简源码
kotlin
suspend fun doSuspend() { delay(1000) }
fun doNormal() { Thread.sleep(1000) }
Q22:协程是轻量级的根本原因?
核心答案
- 用户态调度,无内核态切换开销;
- 协程栈极小(几百字节),线程栈 MB 级;
- 复用线程,1个线程可跑千万协程。
流程图
精简源码
kotlin
repeat(100000) { scope.launch { delay(1000) } }
Q23:Android 协程性能优化要点?
核心答案
- 严格用
Dispatchers.IO/Default; - 并发用
async并行; - 使用官方生命周期 Scope;
- 循环检查
isActive; - Flow 使用
flowOn分离线程。
流程图
精简源码
kotlin
suspend fun fetchMultiData() = coroutineScope {
val d1 = async(Dispatchers.IO) { api1() }
val d2 = async(Dispatchers.IO) { api2() }
Data(d1.await(), d2.await())
}
Q24:Channel 的核心概念与使用场景?send 和 receive 的挂起行为?
核心答案
Channel 是协程间通信的原语,支持挂起。
send(item):通道满时挂起;receive():通道空时挂起 。
适用:生产者-消费者模式。
流程图
精简源码
kotlin
val channel = Channel<Int>(3)
launch { for (i in 1..5) channel.send(i) }
launch { for (v in channel) println(v) }
Q25:select 表达式的作用?如何同时等待多个挂起事件?
核心答案
select 允许同时等待多个挂起操作,最先就绪者被选中 。
用于多 Channel 竞争、带超时的 Deferred 等。
流程图
精简源码
kotlin
select<String> {
ch1.onReceive { "ch1: $it" }
ch2.onReceive { "ch2: $it" }
onTimeout(1000) { "Timeout" }
}
Q26:协程上下文的底层数据结构?Element 和 Key 的关系?
核心答案
CoroutineContext 是由 Element 组成的持久化有序集合 ,每个 Element 通过唯一的 Key 标识。
内部实现类似单链表。加法运算:相同 Key 的后者覆盖前者。
流程图
精简源码
kotlin
val combined = Dispatchers.Main + Job() + CoroutineName("Worker")
val job = combined[Job]
Q27:协程调试与性能分析:如何查看协程的名称、堆栈和调度情况?
核心答案
- 协程名称:
CoroutineName+ JVM 参数-Dkotlinx.coroutines.debug; - 堆栈:启用
-Dkotlinx.coroutines.stacktrace.recovery=true; - 调度:Android Studio Coroutine Debugger / Profiler;
- 日志:
currentCoroutineContext()[CoroutineName]。
流程图
精简源码
kotlin
val scope = CoroutineScope(Dispatchers.Default + CoroutineName("Task"))
scope.launch { println(Thread.currentThread().name) }
Q28:协程的 yield() 函数作用?与 ensureActive() 有何区别?
核心答案
yield():主动让出当前线程(挂起点),避免线程饥饿;ensureActive():仅检查取消,不挂起。
使用建议:大循环中用yield()提升公平性;仅检查取消用ensureActive()。
流程图
精简源码
kotlin
repeat(1000) { if (it % 10 == 0) yield() }
while (true) { ensureActive() }
Q29:协程内部使用 synchronized 会发生什么?为什么不推荐?
核心答案
synchronized 基于线程持有锁 ,协程挂起时锁仍被原线程持有,其他协程在不同线程获取锁会阻塞线程 。
正确做法:使用 Mutex,它在挂起时释放锁的所有权。
流程图
精简源码
kotlin
// 错误
synchronized(lock) { delay(1000) }
// 正确
mutex.withLock { delay(1000) }
Q30:Kotlin 协程在 Compose 中的最佳实践?rememberCoroutineScope 的作用?
核心答案
rememberCoroutineScope() 返回与 Composable 生命周期绑定的 CoroutineScope,离开组合树时自动取消。
用于响应用户事件启动协程。
流程图
精简源码
kotlin
@Composable
fun MyScreen() {
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { /* ... */ } }) { Text("Click") }
}
Q31:协程在 Kotlin Multiplatform Mobile 中的线程模型差异?
核心答案
- Android/JVM:
Main,IO,Default正常。 - iOS (Native):
Main对应主队列;没有内置IO/Default,可用Dispatchers.Default(基于NSOperationQueue)或自定义。
最佳实践:expect/actual提供平台特定调度器,共享代码用Dispatchers.Default和Dispatchers.Main。
注意:iOS 上挂起函数切换回主线程需显式使用withContext(Dispatchers.Main)。
流程图
精简源码
kotlin
expect val platformDispatcher: CoroutineDispatcher
// androidMain
actual val platformDispatcher = Dispatchers.IO
// iosMain
actual val platformDispatcher = Dispatchers.Default
suspend fun work() = withContext(platformDispatcher) { }