Android Kotlin 协程面试笔记
一、协程是什么?
协程是 Kotlin 提供的轻量级并发编程方案,核心是 "非阻塞的挂起" ------允许在代码中暂停执行(挂起),之后再恢复执行,全程不会阻塞底层线程。
协程 vs 线程:
- 本质区别:协程是轻量级并发抽象,通过挂起函数实现非阻塞并发,上下文切换由编译器管理;线程是操作系统级的资源
- 内存占用:一个进程可创建数万甚至数十万协程(内存占用仅几KB),而线程创建数量有限(通常数百个,每个线程占几MB)
- 切换开销:协程切换在用户态完成,无需与操作系统交互
二、协程解决了什么痛点?
- 回调地狱:用同步代码风格写异步逻辑,代码从上到下线性执行,可读性极高
- 线程管理复杂 :无需手动创建和管理线程池,通过
Dispatcher声明意图即可 - 资源泄漏 :通过结构化并发,将协程生命周期与业务组件绑定,避免内存泄漏
三、核心组件
1. CoroutineScope(协程作用域)
协程必须在 CoroutineScope 中启动,用于管理协程的生命周期。
kotlin
// 自定义作用域(与Activity生命周期绑定)
private val scope = CoroutineScope(Dispatchers.Main + Job())
常用作用域:
| 作用域 | 说明 |
|---|---|
viewModelScope |
与ViewModel绑定,ViewModel销毁时自动取消 |
lifecycleScope |
与Activity/Fragment生命周期绑定 |
GlobalScope |
全局作用域,生命周期与应用一致(不推荐,易导致内存泄漏) |
2. Dispatcher(调度器)
决定协程运行在哪个线程上:
| 调度器 | 用途 |
|---|---|
Dispatchers.Main |
Android主线程,用于更新UI |
Dispatchers.IO |
IO线程池,适合网络请求、数据库操作 |
Dispatchers.Default |
默认线程池,适合CPU密集型任务 |
Dispatchers.Unconfined |
不限制线程,较少使用 |
3. 协程构建器
| 构建器 | 返回值 | 用途 |
|---|---|---|
launch |
Job |
执行不关心返回结果的异步任务 |
async |
Deferred<T> |
执行需要返回结果的异步任务,通过 await() 获取结果 |
runBlocking |
- | 阻塞当前线程,主要用于测试或main函数 |
kotlin
// launch:启动并忘记
val job = scope.launch {
delay(1000)
updateUI()
}
// async:启动并获取结果
val deferred = scope.async {
fetchData() // 返回 String
}
val result = deferred.await() // 挂起等待结果
四、suspend 挂起函数
- 使用
suspend关键字修饰的函数 - 挂起函数只能在协程体内或其他挂起函数内调用
- 挂起 ≠ 阻塞:挂起不会阻塞线程,线程可以去执行其他任务
挂起和恢复流程:
- 挂起 :遇到挂起点(如
delay、await),保存局部变量和状态到Continuation对象,释放线程 - 执行:异步任务在后台执行
- 恢复 :异步任务完成,调度器调用
continuation.resumeWith(),协程从挂起点继续执行
底层原理 :编译器将 suspend 函数转换为实现了 Continuation 接口的状态机 。label 变量充当程序计数器,决定执行哪个分支。
五、异常处理
1. try-catch
kotlin
scope.launch {
try {
val result = fetchData()
} catch (e: Exception) {
// 处理异常
}
}
2. CoroutineExceptionHandler
用于捕获根协程及其子协程的未处理异常:
kotlin
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Coroutine", "Caught: $exception")
}
val scope = CoroutineScope(Dispatchers.Main + handler)
注意 :launch 的异常会立即传播;async 的异常在调用 await() 时才抛出。
六、协程取消与结构化并发
结构化并发
- 父协程取消时,自动取消所有子协程
- 协程有明确的生命周期作用域,从根源上避免协程泄漏
取消机制
kotlin
val job = scope.launch {
while (isActive) { // 检查取消状态
// 执行任务
}
}
job.cancel() // 取消协程
注意 :调用 cancel() 不会立即终止协程,需要在协程内部配合检查。在 finally 中释放资源时,建议使用 withContext(NonCancellable) 确保执行。
超时控制
kotlin
// 超时抛出 TimeoutCancellationException
withTimeout(5000) {
fetchData()
}
// 超时返回 null
val result = withTimeoutOrNull(5000) {
fetchData()
}
七、Android 实战示例
kotlin
class MainViewModel : ViewModel() {
fun loadData() {
// viewModelScope 与 ViewModel 生命周期绑定
viewModelScope.launch {
// 切换到 IO 线程执行耗时操作
val data = withContext(Dispatchers.IO) {
fetchDataFromNetwork()
}
// 自动回到 Main 线程更新 UI
_uiState.value = data
}
}
}
八、协程间通信
协程间通信主要依赖 Channel(通道) 和 Flow / SharedFlow / StateFlow(热流) 两大体系。
1. Channel(通道)------ 一对一"点对点"通信
Channel 类似于 Java 中的 BlockingQueue,但区别在于它的 send 和 receive 是挂起函数 (非阻塞等待),适合在生产者-消费者场景下传递数据流。
kotlin
// 创建通道(容量为 0 表示必须发收同时准备好,即"会合")
val channel = Channel<String>()
// 生产者协程
val producer = scope.launch {
repeat(3) {
delay(500)
channel.send("Message $it") // 挂起直到有接收者
}
channel.close() // 关闭表示数据发送完毕
}
// 消费者协程
val consumer = scope.launch {
// 方式一:for 循环遍历(通道关闭时自动结束)
for (msg in channel) {
println("Received: $msg")
}
// 方式二:手动 receive(通道无数据且未关闭时挂起)
// val first = channel.receive()
}
关键特性:
- 一对一:每条消息只能被一个消费者消费,消费即移除。
- 背压处理 :消费者处理慢时,
send会自动挂起,天然实现流量控制。 - 容量可选 :可指定缓冲区大小(如
Channel(10)),超过容量时生产者挂起。
面试冷知识 :早期 Kotlin 提供
produce {}和actor {}构建器,但现在更推荐直接用scope.launch+Channel或改用Flow。
2. Flow(冷流)基本使用
Flow 是冷流------没有 collect(收集)时不会执行任何代码,每次 collect 都会重新触发整个数据流。
kotlin
// 1. 定义 Flow(数据生产方)
fun fetchDataFlow(): Flow<Int> = flow {
for (i in 1..3) {
delay(500) // 模拟异步请求
emit(i) // 发射数据
}
}.flowOn(Dispatchers.IO) // 指定生产代码运行在 IO 线程
// 2. 收集 Flow(数据消费方,在 ViewModel 或 Activity 中)
viewModelScope.launch {
fetchDataFlow()
.map { data -> data * 2 } // 中间操作符(变换数据)
.filter { it > 2 } // 过滤
.catch { e -> // 捕获上游异常
_uiState.value = "Error: ${e.message}"
}
.collect { value -> // 终端操作符,触发执行
// 自动在 Main 线程(因为 viewModelScope 默认 Main)
updateUI(value)
}
}
注意点:Flow 是冷流,collect 执行时才生产数据;多个 collect 各自独立互不影响。线程切换用 flowOn(只影响上游),下游 collect 在调用者的协程上下文中执行。
九、小问题
| 问题 | 答案要点 |
|---|---|
| launch 和 async 的区别? | launch 无返回值(返回Job),async 有返回值(返回Deferred,需await获取) |
| Dispatchers 各有什么区别? | Main(UI线程)、IO(IO密集)、Default(CPU密集)、Unconfined(不限制) |
| 挂起和阻塞的区别? | 挂起不阻塞线程,线程可执行其他任务;阻塞会让线程闲置等待 |
| 如何取消协程? | 调用 job.cancel(),协程内部配合检查 isActive |
| 协程异常怎么处理? | try-catch 或 CoroutineExceptionHandler(注意 launch 立即抛出,async 在 await 时抛出) |