一、协程基础概念与原理类真题
真题 1:协程是线程吗?为什么说它是轻量级的?(字节跳动 / 美团)
解答:
- 本质区别 :
线程是操作系统调度的最小单位(内核态),协程是用户态的轻量级执行单元,由协程库(如kotlinx.coroutines
)管理。 - 轻量级核心优势 :
- 内存占用极小 :每个线程默认分配 1MB 栈内存,协程共享宿主线程栈,单个协程仅需几十字节(如
Continuation
对象)。 - 无内核态切换 :协程切换在用户态通过
Continuation
实现,避免线程切换的上下文开销(寄存器保存 / 恢复)。 - 超高并发能力:单线程可运行上万协程,而线程受限于系统资源(如 Android 手机通常仅支持几百个线程)。
- 内存占用极小 :每个线程默认分配 1MB 栈内存,协程共享宿主线程栈,单个协程仅需几十字节(如
代码佐证:
java
// 启动 10万协程(内存无明显波动)
CoroutineScope().launch {
repeat(100_000) { launch { delay(1000) } }
}
真题 2:挂起函数的本质是什么?为什么不能在普通函数中调用?(阿里 / 腾讯)
解答:
-
编译期转换(CPS 模式) :
挂起函数(suspend
)被编译器转换为接收Continuation
参数的函数,通过状态机保存执行现场。
示例 :Kotlinsuspend fun fetchData(): String { /* 耗时操作 */ }
编译后等价于:
Kotlinfun fetchData(continuation: Continuation<String>): Any? { // 保存当前局部变量,挂起时通过 continuation.resume() 恢复 }
-
调用限制 :
挂起函数依赖Continuation
上下文,只能在协程体内或其他挂起函数中调用,避免脱离协程调度机制导致的状态丢失。
反例:
Kotlin
fun normalFunction() {
fetchData() // 报错!必须在协程或另一个挂起函数中调用
}
二、调度器与线程切换类真题
真题 3:Dispatchers.IO 和 Dispatchers.Default 有什么区别?如何选择?
解答:
调度器 | 底层线程池 | 适用场景 | 最佳实践 |
---|---|---|---|
Dispatchers.IO | 共享的 IO 优化线程池 | 磁盘读写、网络请求(IO 密集型) | 耗时 IO 操作(如 Retrofit、文件读取) |
Dispatchers.Default | 共享的 CPU 密集型线程池 | 计算任务(JSON 解析、数据排序) | CPU 耗时操作(避免阻塞 IO 线程) |
核心区别:
Dispatchers.IO
会复用线程,适合处理耗时但不占用 CPU 的操作(如等待网络响应时线程可处理其他任务)。Dispatchers.Default
限制线程数量(默认 CPU 核心数 × 2),避免 CPU 密集型任务占用过多资源。
错误案例:
Kotlin
// 错误:在 IO 调度器执行 CPU 密集型任务(浪费线程池资源)
withContext(Dispatchers.IO) { complexCalculation() }
// 正确:CPU 任务用 Default,IO 任务用 IO
launch(Dispatchers.Default) { complexCalculation() }
launch(Dispatchers.IO) { networkRequest() }
真题 4:withContext 如何实现线程切换?原理是什么?
解答:
- 实现原理 :
withContext(newContext)
创建新的协程上下文,通过CoroutineDispatcher.dispatch
将协程派发到目标线程。
关键步骤 :- 保存当前协程状态(局部变量、挂起点)到
Continuation
对象。 - 调度器(如
Dispatchers.Main
)将Continuation
提交到目标线程(如主线程 Looper)。 - 目标线程恢复协程执行,继续后续逻辑(如 UI 更新)。
- 保存当前协程状态(局部变量、挂起点)到
源码视角(简化版):
Kotlin
suspend fun <T> withContext(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
val oldContext = currentCoroutineContext()
val newContext = oldContext + context
return newContext[ContinuationInterceptor]!!.interceptContinuation { cont ->
// 调度 cont 到新线程
}.invokeCancellable(block)
}
三、作用域与生命周期管理类真题
真题 5:为什么不推荐使用 GlobalScope?如何避免协程泄漏?(字节跳动 / 百度)
解答:
- GlobalScope 风险 :
作用域生命周期与进程绑定,启动的协程不会随组件(如 Activity)销毁而取消,导致内存泄漏(如协程中持有 Activity 引用)。
正确做法:
-
绑定组件生命周期 :
- Activity/Fragment 使用
lifecycleScope
(AndroidX 库提供,自动随组件销毁取消)。 - ViewModel 使用
viewModelScope
(随 ViewModel 销毁取消)。
Kotlin// Activity 中安全启动协程 lifecycleScope.launch(Dispatchers.IO) { val data = fetchData() withContext(Dispatchers.Main) { uiUpdate(data) } }
- Activity/Fragment 使用
-
自定义作用域时手动管理 :
Kotlin// 手动创建作用域,确保调用 cancel() val myScope = CoroutineScope(Job() + Dispatchers.IO) // 取消所有子协程 myScope.cancel()
反例对比:
Kotlin
// 危险!Activity 销毁后协程仍运行
GlobalScope.launch {
delay(10_000)
activity?.showToast("泄漏!") // NPE 风险
}
真题 6:协程的结构化并发是什么?有什么优势?(美团 / 京东)
解答:
-
定义 :协程必须在作用域(
CoroutineScope
)内启动,子协程自动继承父作用域的生命周期,父作用域取消时所有子协程同步取消。 -
核心优势:
- 避免泄漏:无需手动管理每个协程的取消,作用域销毁时统一清理。
- 异常传播 :父协程捕获异常时,子协程自动取消(如
launch
中未处理的异常会终止整个作用域)。
示例:
CoroutineScope().launch { // 父协程
launch { // 子协程 1
error("崩溃") // 未处理异常,父协程及所有子协程取消
}
launch { // 子协程 2
delay(1000) // 提前被取消
}
}
四、协程构建器与高级特性类真题
真题 7:launch 和 async 有什么区别?如何获取 async 的返回值?
解答:
构建器 | 阻塞当前线程 | 返回值类型 | 主要用途 | 获取结果方式 |
---|---|---|---|---|
launch |
否 | Job (无返回值) |
启动无需结果的异步任务 | 通过 Job.cancel() |
async |
否 | Deferred<T> |
启动需要返回值的异步任务 | deferred.await() |
使用场景:
Kotlin
// 并行获取两个数据(耗时 1s)
val deferred1 = async { fetchUser() }
val deferred2 = async { fetchOrder() }
val result = deferred1.await() + deferred2.await() // 合并结果
注意 :async
默认以 CoroutineStart.DEFAULT
启动(立即调度),可通过 start = CoroutineStart.LAZY
延迟启动:
Kotlin
val lazyDeferred = async(start = CoroutineStart.LAZY) { heavyTask() }
// 按需启动
if (needData) lazyDeferred.start()
真题 8:如何处理协程中的异常?CoroutineExceptionHandler 如何使用?
解答:
- 三种异常处理方式 :
-
try-catch 块 :捕获当前协程内的异常(不影响其他子协程)。
Kotlinlaunch { try { riskyOperation() } catch (e: Exception) { logError(e) } }
-
作用域异常处理器 :通过
CoroutineScope
构造函数传入CoroutineExceptionHandler
。Kotlinval scope = CoroutineScope(CoroutineExceptionHandler { _, e -> handleGlobalError(e) // 处理所有未捕获异常 })
-
全局异常处理 (不推荐):通过
CoroutineExceptionHandler
绑定到Dispatchers.Main
。
-
关键区别:
try-catch
仅捕获当前协程块内的异常。CoroutineExceptionHandler
捕获作用域内所有未处理异常,并终止整个作用域(包括子协程)。
一、Android 实战类真题
真题 1:如何在 Android 中安全地使用协程更新 UI?
解答:
-
线程安全原则 :
Android UI 操作必须在主线程执行,协程通过withContext(Dispatchers.Main)
切换到主线程。
示例代码 :KotlinlifecycleScope.launch(Dispatchers.IO) { val data = fetchDataFromNetwork() // IO 线程 withContext(Dispatchers.Main) { textView.text = data // 安全更新 UI } }
-
原理 :
Dispatchers.Main
封装了主线程的Looper
,通过Handler
将 UI 操作提交到主线程消息队列,避免直接阻塞主线程。 -
关键策略 :
- 显式切换主线程 :耗时操作(如网络请求、数据库查询)在非主线程调度器(
Dispatchers.IO
/Default
)执行,完成后通过withContext(Dispatchers.Main)
切回主线程更新 UI。 - 避免隐性线程风险 :协程默认继承调用者线程,若在非主线程启动协程且未指定调度器,直接操作 UI 会导致崩溃。务必通过
launch(Dispatchers.IO)
或withContext
明确线程分工。
- 显式切换主线程 :耗时操作(如网络请求、数据库查询)在非主线程调度器(
错误案例:
Kotlin
// 危险!在非主线程直接更新 UI(导致崩溃)
launch { textView.text = "错误" } // 未指定调度器,默认使用调用者线程(可能非主线程)
真题 2:协程如何与 Retrofit 结合实现网络请求?
解答:
-
Retrofit 适配协程 :
使用 Retrofit 2.6+ 的suspend
函数支持,直接返回Response
或Result
。
接口定义 :Kotlininterface ApiService { @GET("users") suspend fun getUsers(): Response<List<User>> }
-
协程调用 :
KotlinviewModelScope.launch(Dispatchers.IO) { try { val response = apiService.getUsers() withContext(Dispatchers.Main) { if (response.isSuccessful) { usersLiveData.value = response.body() } else { showError(response.errorBody()) } } } catch (e: Exception) { withContext(Dispatchers.Main) { showError(e) } } }
-
优势 :
避免回调嵌套,代码简洁;自动处理线程切换,保证主线程安全。 -
消除回调地狱 :Retrofit 支持
suspend
函数后,网络请求可直接以同步写法实现异步逻辑,无需嵌套回调。 -
生命周期安全 :在 ViewModel 或组件作用域(
viewModelScope
/lifecycleScope
)内启动协程,框架自动管理协程与组件的生命周期绑定,避免泄漏。 -
最佳实践 :
将网络 / 数据库操作封装为挂起函数,在协程体内通过调度器切换线程,最终通过LiveData
或StateFlow
通知 UI 层,形成「异步处理 - 线程切换 - 响应式更新」的完整链路。
真题 3:如何在 Room 数据库中使用协程?
解答:
-
Room 适配协程 :
在 DAO 中定义suspend
函数,Room 自动生成协程适配代码。
DAO 示例 :Kotlin@Dao interface UserDao { @Query("SELECT * FROM users") suspend fun getUsers(): List<User> }
-
协程调用 :
KotlinviewModelScope.launch(Dispatchers.IO) { val users = userDao.getUsers() withContext(Dispatchers.Main) { usersLiveData.value = users } }
-
性能优化 :
使用Flow
实现响应式数据更新(需结合flowOn(Dispatchers.IO)
切换线程)。Kotlin@Dao interface UserDao { @Query("SELECT * FROM users") fun getUsersFlow(): Flow<List<User>> // 自动适配协程 }
二、性能优化类真题
真题 4:如何优化协程中的分页加载?
解答:
-
分页加载策略 :
- 按需加载:滑动到列表底部时触发下一页请求。
- 并行加载 :使用
async
并行获取多个页面数据(如预加载下一页)。
示例代码:
Kotlinprivate fun loadNextPage(page: Int) { viewModelScope.launch(Dispatchers.IO) { val deferred = async { apiService.getPage(page) } val data = deferred.await() withContext(Dispatchers.Main) { updateUI(data) } } }
-
避免重复请求 :
使用StateFlow
或LiveData
跟踪加载状态,防止多次触发同一页请求。 -
使用 Flow 响应式编程 :
Flow
是协程的数据流处理工具,可通过flowOn
切换线程,collect
收集数据,天然支持背压(Backpressure),适合处理列表分页加载、实时消息同步等场景。例如:- 分页加载时,滑动到列表底部触发
flow.emit(nextPageData)
,自动切换到 IO 线程请求数据,主线程更新 UI。 - 结合
StateFlow
/SharedFlow
实现数据的可观察性,替代传统的LiveData
回调,代码更简洁且线程安全。
- 分页加载时,滑动到列表底部触发
-
并行预加载 :对已知的后续操作(如下一页数据),用
async
并行启动协程,利用等待当前结果的时间预加载数据,减少用户等待时间。
真题 5:如何处理协程中的内存泄漏?
解答:
-
结构化并发原则 :
协程必须绑定到组件生命周期(如lifecycleScope
/viewModelScope
),避免使用GlobalScope
。
正确做法 :Kotlin// Activity 中使用 lifecycleScope lifecycleScope.launch { /* 协程任务 */ } // ViewModel 中使用 viewModelScope viewModelScope.launch { /* 协程任务 */ }
-
上下文管理 :
单例中避免持有 Activity 上下文,优先使用 Application 上下文。
反例 :Kotlin// 危险!单例持有 Activity 上下文导致泄漏 class BadSingleton(private val context: Context) { init { /* 初始化操作 */ } }
正例 :
Kotlinclass GoodSingleton(private val context: Context) { private val appContext = context.applicationContext // 使用 Application 上下文 }
真题 6:如何优化协程的启动性能?
解答:
- 按任务类型选择调度器 :
- IO 密集型任务(网络 / 磁盘) :用
Dispatchers.IO
,其内部线程池会复用线程,避免频繁创建开销(如网络请求等待时线程可处理其他任务)。 - CPU 密集型任务(数据解析 / 计算) :用
Dispatchers.Default
,其线程数限制为 CPU 核心数 × 2,防止过度占用 CPU 资源。 - UI 操作 :必须用
Dispatchers.Main
,通过主线程Looper
安全更新界面。
- IO 密集型任务(网络 / 磁盘) :用
- 减少上下文切换 :避免在协程体内频繁调用
withContext
切换线程,尽量在同一调度器内完成相关操作(如先在 IO 线程读取文件,再在同线程解析数据,最后切回主线程)。
-
延迟启动(LAZY 模式) :
使用async(start = CoroutineStart.LAZY)
延迟执行耗时操作,减少初始化开销。Kotlinval lazyDeferred = async(start = CoroutineStart.LAZY) { loadHeavyData() } // 按需启动 if (needData) lazyDeferred.start()
-
复用线程池 :
避免频繁创建新线程,使用Dispatchers.IO
或自定义线程池。
自定义线程池 :Kotlinval ioDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher() // 使用 ioDispatcher 启动协程 CoroutineScope(ioDispatcher).launch { /* 任务 */ }
-
减少上下文切换 :
在协程体内尽可能完成同一线程的任务,避免频繁调用withContext
。
三、高级实战与性能优化真题
真题 7:如何实现协程与 LiveData 的高效结合?(阿里 / 字节跳动)
解答:
-
LiveData 包装协程结果 :
使用LiveData
的postValue
或setValue
在主线程更新数据。
示例代码 :Kotlinclass UserViewModel : ViewModel() { private val _users = MutableLiveData<List<User>>() val users: LiveData<List<User>> = _users fun fetchUsers() { viewModelScope.launch(Dispatchers.IO) { val data = apiService.getUsers() _users.postValue(data) // 非主线程调用 postValue } } }
-
结合 Flow :
使用flow
发射数据流,通过flowOn
切换线程,collect
到LiveData
。Kotlinclass UserViewModel : ViewModel() { val usersFlow = flow { emit(apiService.getUsers()) }.flowOn(Dispatchers.IO) fun observeUsers(owner: LifecycleOwner, observer: Observer<List<User>>) { usersFlow.launchIn(viewModelScope).collect { _users.postValue(it) } } }
真题 8:如何处理协程中的耗时操作导致的 ANR?
解答:
-
避免阻塞主线程 :
耗时操作(如文件读写、网络请求)必须在非主线程执行。
错误案例 :Kotlin// 危险!在主线程执行耗时操作(导致 ANR) runBlocking { delay(10_000) // 阻塞主线程 10 秒 }
正确做法 :
KotlinviewModelScope.launch(Dispatchers.IO) { delay(10_000) // IO 线程执行耗时操作 withContext(Dispatchers.Main) { /* 更新 UI */ } }
-
超时控制 :
使用withTimeout
限制协程执行时间,防止长时间阻塞。KotlinviewModelScope.launch(Dispatchers.IO) { try { withTimeout(5_000) { // 超时时间 5 秒 val data = apiService.getUsers() } } catch (e: TimeoutCancellationException) { withContext(Dispatchers.Main) { showTimeoutError() } } }
-
严格分离主线程任务 :任何耗时超过 16ms(60fps 要求)的操作必须在非主线程调度器执行,通过
launch(Dispatchers.IO)
或withContext
明确切换。 -
设置超时控制 :使用
withTimeout
限制协程执行时间,避免因网络延迟、数据异常等导致的长时间阻塞。例如:withTimeout(5000) { /* 网络请求或复杂计算 */ }
超时后协程自动取消,防止主线程因等待结果而卡顿。
-
避免阻塞式 API :协程中优先使用挂起函数(如
Retrofit
的suspend
方法),而非阻塞式的execute()
或get()
,确保任务以非阻塞方式挂起,释放线程资源。