Android面试冲击附答案(七)————Coroutines

3 阅读6分钟

Android面试冲击附答案(七)————Coroutines


一、面试题与答案

1. 什么是 Kotlin 协程?它和线程的本质区别是什么?协程为什么说是轻量级线程?轻量体现在哪?
  • 协程是基于线程的、可挂起非阻塞的异步编程方案。
  • 与线程本质区别:
    1. 阻塞方式不同:线程阻塞会占住线程;协程挂起会释放线程。
    2. 调度层级不同:线程由OS内核调度;协程由应用层/JVM用户态调度。
    3. 资源开销不同:协程创建、销毁、切换开销更低。
    4. 生命周期管理:协程更容易做取消与结构化管理。
    5. 可用“同步写法写异步代码”。
2. 什么是挂起函数(suspend)?挂起和阻塞有什么区别?
  • suspend 修饰的是挂起函数,只能在协程/挂起上下文调用。
  • 挂起:只暂停当前协程,不阻塞线程。
  • 阻塞:卡住整个线程,线程无法执行其他任务。
3. 协程如何在不阻塞线程前提下实现异步(suspend 原理)?

核心是:suspend挂起 + 状态机 + Continuation + 回调恢复 + 线程复用

  • suspend 编译后会引入 Continuation 参数。
  • 挂起时保存上下文与断点,释放线程。
  • 异步任务完成后,通过 continuation.resumeWith(...) 恢复执行。
4. 什么是结构化并发(Structured Concurrency)?

结构化并发强调协程的父子层级作用域约束

  1. 协程必须在 CoroutineScope 中启动。
  2. 子协程受父协程生命周期管理。
  3. 取消与异常按层级传播。
5. 协程作用域取消后,子协程会怎样?
  • 一般父协程取消,子协程也会取消。
  • 协程是协作式取消:要在挂起点或主动检查点响应取消。
  • 常见取消检查点:delaywithContextyieldensureActiveisActive
6. CPU密集型任务如何取消?

CPU 密集循环通常无挂起点,需主动插入取消检查:

  1. isActive
  2. ensureActive()
  3. yield()
7. 什么是 Continuation?有什么作用?

Continuation(续体)保存协程恢复所需信息,是挂起恢复机制核心:

interface Continuation<in T> {
    val context: CoroutineContext
    fun resumeWith(result: Result<T>)
}
8. GlobalScope、MainScope、lifecycleScope、viewModelScope 区别?
  • GlobalScope:全局生命周期,不受组件销毁控制,谨慎使用。
  • MainScopeDispatchers.Main + SupervisorJob(),需手动 cancel()
  • lifecycleScope:绑定 LifecycleOwner,销毁自动取消。
  • viewModelScope:绑定 ViewModel 生命周期,配置变更不取消。
9. viewModelScope 默认线程?直接做耗时任务风险?
  • 默认在 主线程
  • 挂起函数本身不阻塞线程,但普通同步耗时逻辑会卡主线程,可能导致 ANR
10. lifecycleScope 和 repeatOnLifecycle 为什么常配合?
  • lifecycleScope 主要在 onDestroy 才整体结束。
  • repeatOnLifecycle 可按可见状态自动启动/取消收集,避免后台空转。
11. CoroutineContext 是什么数据结构?核心元素有哪些?

不可变、可组合、基于 key 的上下文集合。 核心元素:JobDispatcherCoroutineNameCoroutineExceptionHandler 等。

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 生命周期状态有哪些?

典型状态:NewActiveCompletingCompletedCancellingCancelled

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无挂起点死循环