Android面试冲击附答案(七)————Coroutines
一、面试题与答案
1. 什么是 Kotlin 协程?它和线程的本质区别是什么?协程为什么说是轻量级线程?轻量体现在哪?
- 协程是基于线程的、可挂起、非阻塞的异步编程方案。
- 与线程本质区别:
- 阻塞方式不同:线程阻塞会占住线程;协程挂起会释放线程。
- 调度层级不同:线程由OS内核调度;协程由应用层/JVM用户态调度。
- 资源开销不同:协程创建、销毁、切换开销更低。
- 生命周期管理:协程更容易做取消与结构化管理。
- 可用“同步写法写异步代码”。
2. 什么是挂起函数(suspend)?挂起和阻塞有什么区别?
- 被
suspend修饰的是挂起函数,只能在协程/挂起上下文调用。 - 挂起:只暂停当前协程,不阻塞线程。
- 阻塞:卡住整个线程,线程无法执行其他任务。
3. 协程如何在不阻塞线程前提下实现异步(suspend 原理)?
核心是:suspend挂起 + 状态机 + Continuation + 回调恢复 + 线程复用。
suspend编译后会引入Continuation参数。- 挂起时保存上下文与断点,释放线程。
- 异步任务完成后,通过
continuation.resumeWith(...)恢复执行。
4. 什么是结构化并发(Structured Concurrency)?
结构化并发强调协程的父子层级与作用域约束:
- 协程必须在
CoroutineScope中启动。 - 子协程受父协程生命周期管理。
- 取消与异常按层级传播。
5. 协程作用域取消后,子协程会怎样?
- 一般父协程取消,子协程也会取消。
- 协程是协作式取消:要在挂起点或主动检查点响应取消。
- 常见取消检查点:
delay、withContext、yield、ensureActive、isActive。
6. CPU密集型任务如何取消?
CPU 密集循环通常无挂起点,需主动插入取消检查:
isActiveensureActive()yield()
7. 什么是 Continuation?有什么作用?
Continuation(续体)保存协程恢复所需信息,是挂起恢复机制核心:
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
8. GlobalScope、MainScope、lifecycleScope、viewModelScope 区别?
GlobalScope:全局生命周期,不受组件销毁控制,谨慎使用。MainScope:Dispatchers.Main + SupervisorJob(),需手动cancel()。lifecycleScope:绑定LifecycleOwner,销毁自动取消。viewModelScope:绑定ViewModel生命周期,配置变更不取消。
9. viewModelScope 默认线程?直接做耗时任务风险?
- 默认在 主线程。
- 挂起函数本身不阻塞线程,但普通同步耗时逻辑会卡主线程,可能导致 ANR。
10. lifecycleScope 和 repeatOnLifecycle 为什么常配合?
lifecycleScope主要在onDestroy才整体结束。repeatOnLifecycle可按可见状态自动启动/取消收集,避免后台空转。
11. CoroutineContext 是什么数据结构?核心元素有哪些?
是不可变、可组合、基于 key 的上下文集合。
核心元素:Job、Dispatcher、CoroutineName、CoroutineExceptionHandler 等。
12. CoroutineContext 的 + 做了什么?冲突时谁覆盖?
+表示上下文合并。- 同 key 冲突时:右侧覆盖左侧。
13. 子协程会继承父协程哪些上下文元素?
- 基本继承父
CoroutineContext。 Job不直接复用,会创建子 Job 并建立父子关系。
14. Dispatchers.Main / IO / Default / Unconfined 区别?
Main:主线程(UI)。IO:IO 密集任务。Default:CPU 密集计算。Unconfined:线程不可预测,生产慎用。
15. Dispatchers.Main 与 Main.immediate 区别?
- 不在主线程时,二者都切回主线程队列。
- 已在主线程时,
Main.immediate可立即执行,Main通常入队。
16. withContext 和 launch 在线程切换语义区别?
withContext:挂起等待并返回结果。launch:异步启动,无返回值(返回Job)。
17. 为什么 suspend 函数里更推荐 withContext 而不是再 launch?
withContext保证结构化串行语义,异常可被当前调用链处理。- 直接
launch可能导致外层无法感知完成时机与异常路径。
18. Job 生命周期状态有哪些?
典型状态:New、Active、Completing、Completed、Cancelling、Cancelled。
19. join() / cancel() / cancelAndJoin() / invokeOnCompletion() 区别?
join():挂起等待结束。cancel():发取消信号。cancelAndJoin():先取消再等结束。invokeOnCompletion():注册终态回调。
20. 父 Job 与子 Job 的取消传播规则?
- 父取消会递归取消子。
- 普通
Job下子异常常会影响同级与父级。 SupervisorJob可隔离兄弟协程失败影响。
21. SupervisorJob 和普通 Job 核心区别?
- 普通
Job:子失败可能导致整棵树取消。 SupervisorJob:失败局部化,默认不向兄弟扩散。
22. runBlocking / launch / async 区别?
runBlocking:阻塞线程,用于桥接非协程环境,主线程禁用。launch:返回Job,无结果。async:返回Deferred<T>,通过await()取结果。
23. CoroutineStart 四种模式含义?
DEFAULT:立即调度。LAZY:惰性启动,需start/join/await触发。ATOMIC:到首个挂起点前不可取消。UNDISPATCHED:当前线程立即执行到首挂起点。
24. 为什么说协程取消是“协作式取消”?
取消不是强杀线程,而是打取消标记;协程在挂起点/检查点感知后自行结束。
25. ensureActive()、yield()、isActive 适用场景?
isActive:循环条件判断。ensureActive():主动检查并抛取消异常。yield():让出线程并顺带检查取消。
26. CancellationException 为什么要特殊对待?
它是正常取消信号,不是普通业务错误;吞掉会破坏取消链路,导致资源泄露风险。
27. finally 中执行挂起操作为何要用 NonCancellable?
取消态下普通挂起函数会立刻抛 CancellationException。
需用 withContext(NonCancellable) 保证关键清理逻辑(如持久化/释放资源)可执行。
28. launch 和 async 在异常处理差异?
launch:异常通常立即上抛到父层。async:异常延迟到await()才抛出。
29. 协程并发下为什么仍有线程安全问题?
协程最终跑在线程上,多协程访问共享可变状态仍可能竞态。 可用 Mutex/Channel/Actor/原子类/单线程调度器 保护。
30. 协程内存泄露常见原因有哪些?
- 滥用
GlobalScope - 自建
CoroutineScope未及时cancel() - 协程持有
Activity/Fragment/View强引用 - CPU 密集任务无取消检查
- 吞掉
CancellationException
二、Coroutines原理
1) 角色说明
| 角色 | 职责 | 关键点 |
|---|---|---|
CoroutineScope | 管理协程生命周期 | 结构化并发入口 |
Job | 取消、状态、父子关系 | 取消与异常传播骨架 |
Dispatcher | 决定执行线程 | Main/IO/Default |
Continuation | 挂起点恢复 | 状态机恢复执行 |
suspend 函数 | 声明可挂起逻辑 | 非阻塞等待 |
2) 挂起恢复流程图
launch/async
|
v
执行到 suspend 点 ----> 保存现场(Continuation)
| |
| v
| 释放当前线程
| |
v v
后台任务完成 <--------- 回调 resumeWith(result)
|
v
调度到 Dispatcher 指定线程
|
v
从挂起点继续执行
3) 取消传播关系
Parent Job cancel
|
+--> Child A cancel
+--> Child B cancel
普通 Job: Child A 异常 -> 可能取消 Parent/Child B
SupervisorJob: Child A 异常 -> 仅 Child A 失败
4) 高频对应关系
| 场景 | 推荐做法 | 避免 |
|---|---|---|
| ViewModel 发请求 | viewModelScope + withContext(IO) | 主线程直接做耗时同步逻辑 |
| 页面收集流 | repeatOnLifecycle | 永久在线 launch { collect } |
| 并发并取结果 | async + awaitAll | 多层嵌套回调 |
| 取消敏感清理 | withContext(NonCancellable) | finally 里直接可取消挂起 |
| CPU密集可取消 | 循环中 ensureActive/yield | 无挂起点死循环 |