12. Android 协程通关秘籍:31 道资深工程师面试题精讲

Q1:结构化并发是什么?核心价值?

核心答案

协程之间形成父子层级关系,遵循三大规则:

  1. 父协程取消 → 所有子协程递归取消;
  2. 子协程异常 → 父协程及兄弟协程全部取消(除非使用 SupervisorJob);
  3. 父协程等待所有子协程完成。
    价值:杜绝内存泄漏、避免异常丢失、简化管理

流程图

graph TD A[父协程] --> B[子协程1] A --> C[子协程2] D[父取消] --> B D --> C E[子异常] --> A A --> B A --> C

精简源码

kotlin 复制代码
scope.launch {
    launch { /* 子协程1 */ }
    async { /* 子协程2 */ }
}
// 父协程取消,两个子协程自动取消

Q2:什么是 Kotlin 协程?和线程的核心区别?

核心答案

协程是用户态轻量级执行单元 ,由 Kotlin 编译器+运行时管理,不依赖操作系统内核 ;线程是内核态调度,切换成本高;协程挂起不阻塞线程 ,千万协程可稳定运行。核心区别:调度层级、资源开销、阻塞/挂起模型

流程图

graph TD A[线程] --> B[内核调度
高开销
阻塞线程] C[协程] --> D[用户态调度
极低开销
挂起不阻塞]

精简源码

kotlin 复制代码
GlobalScope.launch {
    delay(1000) // 挂起,不阻塞线程
    println("协程")
}
Thread {
    Thread.sleep(1000) // 阻塞线程
    println("线程")
}.start()

Q3:协程的挂起与恢复原理是什么?

核心答案

挂起函数通过CPS 变换(Continuation Passing Style) ,将执行状态(局部变量、指令位置)保存到 Continuation;挂起时切出线程,恢复时从状态点继续执行,无线程阻塞 。核心:状态保存 + 线程切出 + 状态恢复

流程图

graph TD A[执行挂起函数] --> B[保存状态到Continuation] B --> C[挂起,释放线程] C --> D[异步操作完成] D --> E[恢复状态,继续执行]

精简源码

kotlin 复制代码
suspend fun fetchData() {
    val data = withContext(Dispatchers.IO) { "网络数据" }
    println(data)
}

Q4:CoroutineScope 作用?为什么必须用?

核心答案

统一管理协程生命周期 ,实现结构化并发 ;作用:自动取消子协程、防止内存泄漏、统一异常管理 。禁止使用 GlobalScope(无生命周期,内存泄漏风险)。

流程图

graph TD A[CoroutineScope] --> B[管理子协程] B --> C[页面销毁→取消所有协程] A --> D[统一异常/取消]

精简源码

kotlin 复制代码
class MyViewModel : ViewModel() {
    fun load() = viewModelScope.launch { } // ViewModel销毁自动取消
}
// 禁止:GlobalScope.launch { }

Q5:多个挂起函数顺序调用时,状态机(label)如何工作?挂起和恢复的具体流程是什么?

核心答案

挂起函数被编译为状态机 。每当调用一个子挂起函数前,父协程的 label 会递增,然后调用子函数。如果子函数挂起(返回 COROUTINE_SUSPENDED),父函数也立即挂起,保留当前 label。当子函数恢复时,会回调父函数的 continuation.resumeWith(),重新进入父函数的状态机,根据 label 跳转到下一个挂起点之后继续执行。

流程图

graph TD A[父协程 label=0] --> B[执行代码到挂起前] B --> C[label++ 并调用子挂起函数] C --> D[子函数挂起,返回 COROUTINE_SUSPENDED] D --> E[父函数也返回 COROUTINE_SUSPENDED] E --> F[子函数恢复,回调父的resumeWith] F --> G[父再次进入,label已更新,跳转到后续] G --> H[继续执行,可能再次挂起或结束]

精简源码

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;
  • 协程取消是协作式,必须检查取消状态。

流程图

graph TD A[New] --> B[Active] B --> C[Cancelling] --> D[Cancelled] B --> E[Completing] --> F[Completed]

精简源码

kotlin 复制代码
val job = scope.launch {
    while (isActive) { /* 任务 */ }
}
job.cancel()

Q7:协程取消是协作式,什么意思?如何保证取消?

核心答案

协程不会强制终止,必须主动检查取消状态才能停止;保证方式:

  1. 使用 isActive 判断;
  2. 使用挂起函数(delay/withContext 自动响应取消);
  3. 循环任务中调用 yield()/ensureActive()

流程图

graph TD A[调用cancel] --> B[标记取消状态] B --> C[协程检查isActive] C --> D[停止执行]

精简源码

kotlin 复制代码
launch {
    for (i in 0..100) {
        ensureActive()
        println(i)
    }
}

Q8:SupervisorJob 作用?和普通 Job 区别?

核心答案
监督式 Job :子协程异常不影响父协程和其他子协程;普通 Job:子协程异常 → 传播到父 → 所有协程取消。适用:独立任务(如多个独立接口请求)。

流程图

graph TD A[普通Job] --> B[子异常→全部取消] C[SupervisorJob] --> D[子异常→仅自身取消]

精简源码

kotlin 复制代码
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch { /* 崩溃不影响兄弟 */ }
scope.launch { /* 正常执行 */ }

Q9:协程异常传播机制?

核心答案

  1. 根协程(launch):异常直接抛出崩溃;
  2. async 根协程 :异常在 await() 时抛出;
  3. 子协程:异常向上传播,取消整个父子树;
  4. SupervisorJob:异常仅在自身协程内,不传播。

流程图

graph TD A[子协程异常] --> B[普通Job→传播父→全部取消] A --> C[SupervisorJob→不传播]

精简源码

kotlin 复制代码
scope.launch { error("崩溃") }
scope.launch(SupervisorJob()) { error("仅自身崩溃") }

Q10:CoroutineExceptionHandler 使用场景与限制?

核心答案

全局异常捕获,处理未捕获的协程异常;限制:

  1. 只能用在根协程
  2. 不能捕获 async 根协程异常;
  3. 不能捕获 try-catch 已处理的异常。

流程图

graph TD A[协程未捕获异常] --> B[CoroutineExceptionHandler捕获] B --> C[上报/日志,不崩溃]

精简源码

kotlin 复制代码
val handler = CoroutineExceptionHandler { _, e -> log(e) }
scope.launch(handler) { error("被捕获") }

Q11:协程中 try-catch 最佳实践?

核心答案

  1. 子协程异常:必须在子协程内部 catch,外部无法捕获;
  2. async 异常:在 await() 处 catch;
  3. 独立任务:用 SupervisorJob + 内部 try-catch。

流程图

graph TD A[子协程] --> B[内部try-catch] C[async] --> D[await处try-catch]

精简源码

kotlin 复制代码
scope.launch {
    launch {
        try { error("") } catch (e: Exception) { }
    }
}

Q12:launch 和 async 区别?

核心答案

  • launch:无返回值,启动任务,返回 Job
  • async:有返回值,返回 Deferred,必须用 await() 获取结果;
  • 并发执行用 async,顺序执行用 launch

流程图

graph TD A[launch] --> B[无返回值
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() 更轻量,无额外开销,是官方推荐线程切换方案。

流程图

graph TD A[原线程] --> B[withContext
切换调度器] B --> C[执行任务] C --> D[自动切回原线程]

精简源码

kotlin 复制代码
suspend fun getData() = withContext(Dispatchers.IO) { api.getData() }

Q14:Android 四大调度器区别与使用场景?

核心答案

  1. Main:主线程,UI操作;
  2. IO:IO密集型(网络、数据库),线程池复用;
  3. Default:CPU密集型(计算、解析),核心数线程;
  4. Unconfined :不绑定线程,恢复线程不确定,禁止业务使用

流程图

graph TD A[Dispatchers.Main] --> B[UI操作] C[Dispatchers.IO] --> D[网络/文件] E[Dispatchers.Default] --> F[计算/解析]

精简源码

kotlin 复制代码
lifecycleScope.launch {
    val data = withContext(Dispatchers.IO) { api.getData() }
    binding.tv.text = data
}

Q15:Android 协程内存泄漏场景及解决方案?

核心答案

泄漏场景:使用 GlobalScope;自定义 Scope 未绑定生命周期;协程持有 View/Activity 引用,页面销毁未取消。

方案:使用 lifecycleScope/viewModelScope;自定义 Scope 在销毁时 cancel()

流程图

graph TD A[内存泄漏] --> B[GlobalScope] A --> C[未绑定生命周期] B --> D[使用官方Scope] C --> D

精简源码

kotlin 复制代码
class MyFragment : Fragment() {
    override fun onViewCreated() {
        viewLifecycleOwner.lifecycleScope.launch { }
    }
}

Q16:viewModelScope / lifecycleScope 原理?

核心答案

官方扩展 Scope,自动绑定生命周期

  • viewModelScope:ViewModel 销毁时 cancel()
  • lifecycleScope:生命周期 DESTROYED 时 cancel()
    底层:CoroutineScope + SupervisorJob() + 生命周期监听。

流程图

graph TD A[ViewModel销毁] --> B[viewModelScope.cancel] C[页面onDestroy] --> D[lifecycleScope.cancel]

精简源码

kotlin 复制代码
viewModelScope.launch { }

Q17:StateFlow 和 SharedFlow 区别?

核心答案

  • StateFlow :有初始值 ,粘性,只保留最新值,用于UI状态
  • SharedFlow :无初始值,可配置重放缓存,用于事件(点击、Toast);
  • 都是热流,观察者越多越高效。

流程图

graph TD A[StateFlow] --> B[初始值
粘性
状态管理] C[SharedFlow] --> D[无初始值
可配置重放
事件管理]

精简源码

kotlin 复制代码
val uiState = MutableStateFlow(UiState.Loading)
val event = MutableSharedFlow<Event>()

Q18:Flow 背压处理方式?

核心答案

背压:生产者速度 > 消费者速度;处理方式:

  1. buffer():缓存队列;
  2. conflate():只保留最新值;
  3. collectLatest:取消旧收集,只处理最新;
  4. flowOn:指定调度器隔离。

流程图

graph TD A[背压] --> B[buffer 缓存] A --> C[conflate 取最新] A --> D[collectLatest 取消旧任务]

精简源码

kotlin 复制代码
flow { emit(1) }.conflate().collect { }

Q19:Flow 的 catch 操作符与 try-catch 在收集处的区别?如何恢复异常后的发射?

核心答案

  • catch 操作符:捕获上游 (在 catch 之前)的异常,可以重新发射替代值、重试或抛出;
  • try-catch 包裹 collect:捕获下游 的异常(收集函数内),无法处理上游的发射异常。
    恢复异常后的发射:catch 中可以 emit 回退值,然后正常结束。

流程图

graph TD A[上游flow] -->|异常| B{catch} B -->|捕获| C[emit fallback] C --> D[collect收到fallback]

精简源码

kotlin 复制代码
flow { emit(1); throw IOException() }
    .catch { e -> emit(-1) }
    .collect { println(it) } // 1, -1

Q20:flatMapConcat、flatMapMerge 与 flatMapLatest 的区别?

核心答案

  • flatMapConcat串行执行,前一个内层 flow 完全结束后才收集下一个;
  • flatMapMerge并发执行,可指定并发数;
  • flatMapLatest只关心最新 ,新元素到达时取消上一个内层 flow。
    适用场景:顺序处理、多个独立异步任务、搜索联想词。

流程图

graph TD A[元素序列: A B C] --> B[flatMapConcat: A全部→B全部→C全部] A --> C[flatMapMerge: 同时收集A,B,C] A --> D[flatMapLatest: 收集A → B到来取消A → 收集B]

精简源码

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 关键字作用?没有它会怎样?

核心答案

  1. 标记函数为挂起函数,只能在协程/其他挂起函数中调用;
  2. 触发编译器生成状态机代码,实现挂起恢复;
  3. 没有 suspend,无法挂起,只能同步阻塞。

流程图

graph TD A[suspend关键字] --> B[编译器生成状态机] B --> C[支持挂起/恢复] A --> D[限制调用环境]

精简源码

kotlin 复制代码
suspend fun doSuspend() { delay(1000) }
fun doNormal() { Thread.sleep(1000) }

Q22:协程是轻量级的根本原因?

核心答案

  1. 用户态调度,无内核态切换开销;
  2. 协程栈极小(几百字节),线程栈 MB 级;
  3. 复用线程,1个线程可跑千万协程

流程图

graph TD A[协程轻量] --> B[用户态调度] A --> C[极小栈内存] A --> D[线程高度复用]

精简源码

kotlin 复制代码
repeat(100000) { scope.launch { delay(1000) } }

Q23:Android 协程性能优化要点?

核心答案

  1. 严格用 Dispatchers.IO/Default
  2. 并发用 async 并行;
  3. 使用官方生命周期 Scope;
  4. 循环检查 isActive
  5. Flow 使用 flowOn 分离线程。

流程图

graph TD A[优化] --> B[合理调度器] A --> C[async并发] A --> D[及时取消] A --> E[结构化并发]

精简源码

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():通道空时挂起
    适用:生产者-消费者模式。

流程图

graph TD A[生产者] -->|send| B[Channel] B -->|receive| C[消费者] B --> D{容量满?} -->|是| E[send挂起] B --> F{容量空?} -->|是| G[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 等。

流程图

graph TD A[select] --> B[case1.onReceive] A --> C[case2.onReceive] A --> D[timeout] B --> E[等待,第一个就绪执行] C --> E D --> E

精简源码

kotlin 复制代码
select<String> {
    ch1.onReceive { "ch1: $it" }
    ch2.onReceive { "ch2: $it" }
    onTimeout(1000) { "Timeout" }
}

Q26:协程上下文的底层数据结构?Element 和 Key 的关系?

核心答案
CoroutineContext由 Element 组成的持久化有序集合 ,每个 Element 通过唯一的 Key 标识。

内部实现类似单链表。加法运算:相同 Key 的后者覆盖前者。

流程图

graph TD A[CoroutineContext] --> B[Element A: Job] B --> C[next: Element B: Dispatcher] C --> D[next: Element C: CoroutineName]

精简源码

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]

流程图

graph TD A[启用调试JVM参数] --> B[协程带CoroutineName] B --> C[日志输出协程名] B --> D[Android Studio协程调试器]

精简源码

kotlin 复制代码
val scope = CoroutineScope(Dispatchers.Default + CoroutineName("Task"))
scope.launch { println(Thread.currentThread().name) }

Q28:协程的 yield() 函数作用?与 ensureActive() 有何区别?

核心答案

  • yield()主动让出当前线程(挂起点),避免线程饥饿;
  • ensureActive():仅检查取消,不挂起。
    使用建议:大循环中用 yield() 提升公平性;仅检查取消用 ensureActive()

流程图

graph TD A[yield] --> B[挂起协程,让出线程] B --> C[调度器选下一个协程] D[ensureActive] --> E[仅检查取消,不挂起] E -->|已取消| F[抛出CancellationException]

精简源码

kotlin 复制代码
repeat(1000) { if (it % 10 == 0) yield() }
while (true) { ensureActive() }

Q29:协程内部使用 synchronized 会发生什么?为什么不推荐?

核心答案
synchronized 基于线程持有锁 ,协程挂起时锁仍被原线程持有,其他协程在不同线程获取锁会阻塞线程

正确做法:使用 Mutex,它在挂起时释放锁的所有权。

流程图

graph TD A[synchronized] --> B[协程挂起] B --> C[线程仍持有锁] C --> D[其他协程阻塞线程] E[Mutex] --> F[协程挂起时释放Mutex] F --> G[其他协程可获取]

精简源码

kotlin 复制代码
// 错误
synchronized(lock) { delay(1000) }
// 正确
mutex.withLock { delay(1000) }

Q30:Kotlin 协程在 Compose 中的最佳实践?rememberCoroutineScope 的作用?

核心答案
rememberCoroutineScope() 返回与 Composable 生命周期绑定的 CoroutineScope,离开组合树时自动取消。

用于响应用户事件启动协程。

流程图

graph TD A[Button点击] --> B[rememberCoroutineScope().launch] B --> C[执行suspend操作] C --> D[更新State] E[Composable离开] --> F[scope自动取消]

精简源码

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.DefaultDispatchers.Main
    注意:iOS 上挂起函数切换回主线程需显式使用 withContext(Dispatchers.Main)

流程图

graph TD A[expect platformDispatcher] --> B[androidMain: Dispatchers.IO] A --> C[iosMain: Dispatchers.Default] D[共享代码调用] --> E[平台特定执行]

精简源码

kotlin 复制代码
expect val platformDispatcher: CoroutineDispatcher
// androidMain
actual val platformDispatcher = Dispatchers.IO
// iosMain
actual val platformDispatcher = Dispatchers.Default
suspend fun work() = withContext(platformDispatcher) { }

相关推荐
Dlrb12111 小时前
C语言-字符串指针与函数指针
java·c语言·前端
PBitW1 小时前
组件封装注意事项
前端·vue.js
weiggle1 小时前
Android 输入事件分发流程:从物理触控到 Activity 的完整旅程
前端
m0_739030001 小时前
[特殊字符] Java 高频面试题汇总
java·开发语言·面试
白宇横流学长2 小时前
基于Spring Boot的校园考勤管理系统的设计与实现
java·spring boot·后端
研究点啥好呢2 小时前
海康威视 机器人嵌入式软件工程师 面试题精选:10道高频考题+答案解析
ai·面试·机器人·自动化·求职招聘
yingyima2 小时前
开发者必备在线工具集合 2025:实战案例解析
前端
前端毕业班2 小时前
面试官:实现一个带类型约束的 EventEmitter
前端·面试
ReSearch2 小时前
sfsEdgeStore:边缘计算时代的轻量级数据存储解决方案
数据库·后端·github