Android面试(五):深入Kotlin协程
一、协程本质解析:轻量级线程的真相
协程并非传统意义上的线程,而是线程框架之上的任务调度单元。其轻量性体现在:
// 启动10万个协程对比线程
fun main() = runBlocking {
repeat(100_000) { // 协程版本
launch {
delay(1000L)
print(".")
}
}
// 对比线程版本(将导致OOM)
// repeat(100_000) {
// thread {
// Thread.sleep(1000)
// print(".")
// }
// }
}
✅ 执行分析:
- 协程仅消耗KB级内存,线程需要MB级
- 协程切换在用户态完成,无需内核介入
- 挂起函数通过CPS(Continuation Passing Style)实现状态保存
二、结构化并发:协程的骨架系统
2.1 协程作用域层级
2.2 生命周期绑定实践
class MyActivity : AppCompatActivity(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate()
job = Job()
// 启动网络请求协程
launch {
val data = withContext(Dispatchers.IO) {
apiService.fetchData()
}
updateUI(data)
}
}
override fun onDestroy() {
super.onDestroy()
job.cancel() // 自动取消所有子协程
}
}
⚠️ 典型错误:在ViewModel中使用GlobalScope
将导致:
- 无法自动取消任务
- 内存泄漏风险增加
- 失去异常传播能力
三、协程上下文:控制并发的DNA
3.1 关键元素组合
val customContext = Dispatchers.Default +
CoroutineName("BackgroundTask") +
CoroutineExceptionHandler { _, e ->
Log.e("Coroutine", "Caught $e")
}
launch(customContext) {
// 在此执行后台计算
}
3.2 调度器选择策略
调度器类型 | 适用场景 | 线程池特征 |
---|---|---|
Dispatchers.Main | UI更新、轻量任务 | 单线程(主线程) |
Dispatchers.IO | 网络/文件操作 | 动态扩容(64线程) |
Dispatchers.Default | CPU密集型计算 | 固定线程(CPU核数) |
四、Flow:响应式编程的协程实现
4.1 冷流与热流对比
// 冷流示例(按需生产)
val coldFlow = flow {
repeat(3) {
emit(it)
delay(100)
}
}
// 热流示例(独立生产)
val stateFlow = MutableStateFlow(0)
fun startProducer() {
viewModelScope.launch {
repeat(Int.MAX_VALUE) {
stateFlow.emit(it)
delay(1000)
}
}
}
4.2 背压处理策略
flow {
for (i in 1..1000) {
emit(i)
}
}
.buffer(50) // 设置缓冲区大小
.conflate() // 保留最新值
.collectLatest { value -> // 取消慢速收集器
// 处理最新值
}
五、协程取消的陷阱与解决方案
5.1 不可取消的代码块
suspend fun criticalTask() = withContext(NonCancellable) {
// 即使父协程取消,也会执行完成
writeToDatabase()
sendLogToServer()
}
5.2 资源清理规范
val job = launch {
try {
val stream = openFileStream()
stream.use { // 使用use函数自动关闭资源
while (isActive) { // 检查取消状态
val data = it.read()
process(data)
}
}
} finally {
withContext(NonCancellable) {
releaseExternalResource() // 确保清理执行
}
}
}
六、协程调试进阶技巧
6.1 协程ID追踪
# 启用协程调试模式
System.setProperty("kotlinx.coroutines.debug", "on")
日志输出示例:
[CoroutineId=3] Starting network request
[CoroutineId=3] Received 2048 bytes
6.2 自定义拦截器
class TimingInterceptor : CoroutineInterceptor {
override fun <T> interceptContinuation(continuation: Continuation<T>) =
object : Continuation<T> {
private val startTime = System.currentTimeMillis()
override val context = continuation.context
override fun resumeWith(result: Result<T>) {
val duration = System.currentTimeMillis() - startTime
log("Coroutine took ${duration}ms")
continuation.resumeWith(result)
}
}
}
七、工业级协程架构模式
7.1 仓库层封装方案
class UserRepository(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
private val scope = CoroutineScope(SupervisorJob() + ioDispatcher)
suspend fun fetchUser(id: String): User =
withContext(scope.coroutineContext) {
// 网络请求与缓存逻辑
}
fun clear() {
scope.cancel("Repository cleared")
}
}
7.2 复杂任务组合
suspend fun loadDashboardData() = coroutineScope {
val userDeferred = async { getUserProfile() }
val notificationsDeferred = async { fetchNotifications() }
val user = userDeferred.await() // 挂起点1
val notifications = notificationsDeferred.await() // 挂起点2
DashboardData(user, notifications).apply {
preprocess() // 同步处理
}
}
八、协程性能优化指南
8.1 调度器优化矩阵
场景 | 推荐方案 | 性能提升点 |
---|---|---|
并行网络请求 | Dispatchers.IO.limitedParallelism(8) | 避免线程过多竞争 |
数据库批量操作 | 使用channelFlow 缓冲写入 | 减少事务提交次数 |
图片处理流水线 | 自定义固定大小线程池 | 控制CPU占用峰值 |
8.2 避免过度切换上下文
// 反模式:频繁切换调度器
suspend fun processData() {
withContext(Dispatchers.IO) { /* 网络请求 */ }
withContext(Dispatchers.Default) { /* 数据处理 */ }
withContext(Dispatchers.Main) { /* UI更新 */ }
}
// 优化方案:批量处理
suspend fun optimizedProcess() = withContext(Dispatchers.Default) {
val rawData = fetchData() // 在IO调度器执行
val processed = transformData(rawData) // 同线程继续处理
withContext(Dispatchers.Main) {
updateUI(processed) // 单次切换
}
}
九、协程在跨平台开发中的实践
9.1 KMM架构中的协程使用
// commonMain模块
expect val backgroundDispatcher: CoroutineDispatcher
class SharedRepository {
suspend fun getData(): Data =
withContext(backgroundDispatcher) {
// 跨平台业务逻辑
}
}
// androidMain模块
actual val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO
// iosMain模块
actual val backgroundDispatcher: CoroutineDispatcher =
Dispatchers.Default.limitedParallelism(1)
十、协程面试真题解析
10.1 问题:如何避免协程内存泄漏?
解决方案:
- 使用
viewModelScope
/lifecycleScope
自动绑定生命周期 - 避免在全局作用域启动长任务
- 使用WeakReference传递上下文
- 定期使用LeakCanary进行检测
10.2 问题:协程挂起函数是否阻塞线程?
深度解析:
suspend fun nonBlockingCall() {
delay(1000) // 挂起协程但不阻塞线程
// 等同于:
// handler.postDelayed({ continuation.resume() }, 1000)
}
fun blockingCall() {
Thread.sleep(1000) // 实际阻塞当前线程
}
✅ 核心区别:
- 挂起函数通过回调机制释放线程资源
- 阻塞调用会持续占用线程直至完成
总结
关键知识点
- 结构化并发体系:通过作用域层级实现协程树管理,确保资源自动释放
- 上下文控制艺术:合理组合调度器、异常处理器等元素构建健壮任务
- 响应式数据流:掌握Flow的冷热流特性及背压处理策略
- 取消传播机制:理解协程取消的双向传播路径及资源清理规范
- 跨平台适配:在KMM中实现协程调度器的平台差异化配置