Kotlin协程实战
真题1:协程与线程的本质区别是什么?为什么说协程是轻量级的?
面试官:
“我看你项目中用协程替代了线程池,能说说协程和线程的核心区别吗?为什么协程更适合高并发?”
候选人:
“协程和线程的区别有点像‘快递员’和‘卡车’的关系。线程是操作系统直接管理的‘大卡车’,每辆卡车得自己带一整个货箱(1MB的栈内存),启动和换路线得经过调度中心(内核态),成本高还慢。而协程更像是快递员,他们骑电动车(用户态调度),一辆卡车能装几千个快递员,换任务时只需要记下当前位置,轻装上阵,内存开销只有几十KB。
比如我们做电商秒杀,10万用户同时抢购。如果用线程池,开500个线程就得吃掉500MB内存,线程切换还得排队等调度,CPU都忙不过来。换成协程,一个线程就能跑几万个请求,内存省了90%,QPS直接翻倍——这就像用电动车送快递,不堵车还省油。”
真题2:GlobalScope为什么会导致内存泄漏?如何正确使用作用域?
面试官:
“你们项目里把GlobalScope全换成了lifecycleScope,是踩过坑吧?”
候选人:
“没错!之前做视频弹幕功能时,用GlobalScope启动了一个无限循环的弹幕请求。结果用户退出了页面,协程还在后台疯狂拉数据,Fragment像僵尸一样赖在内存里,直接导致OOM崩溃。后来发现GlobalScope是‘长生不老’的,它的生命周期和整个App绑定,根本不管Activity的死活。
现在我们用lifecycleScope,相当于给协程装了‘智能开关’。Activity销毁时,自动触发onDestroy
里的取消逻辑。就像给电器装了个漏电保护器——页面一关,所有后台任务立马断电,内存泄漏风险直接清零。
比如在ViewModel里这么写:
viewModelScope.launch {
val data = withContext(Dispatchers.IO) { fetchData() }
_liveData.value = data //自动绑定到ViewModel生命周期
}
就算用户疯狂滑动页面,旧的请求也会被及时取消,再也不用担心后台跑‘幽灵任务’了。”
真题3:如何处理协程中的并发任务?async和launch有什么区别?
面试官:
“如果要同时调三个接口,等结果全到了再更新UI,用协程怎么搞?如果有一个接口挂了怎么办?”
候选人:
“这得请出‘协程三剑客’——async
、await
和coroutineScope
。比如用户主页需要同时拉取用户信息、订单列表和消息通知,可以这么写:
lifecycleScope.launch {
try {
val (user, orders, messages) = coroutineScope {
val userDeferred = async { api.getUser() } // 并行启动
val ordersDeferred = async { api.getOrders() }
val messagesDeferred = async { api.getMessages() }
Triple(userDeferred.await(), ordersDeferred.await(), messagesDeferred.await())
}
updateUI(user, orders, messages) // 三个结果都到了才更新
} catch (e: Exception) {
showErrorToast("有一个接口挂了:${e.message}") // 任一失败都会跳到这里
}
}
这里的关键是coroutineScope
会‘一损俱损’——只要有一个子协程抛异常,整个作用域里的任务全取消。而async和launch的核心区别在于‘带不带回执’——async返回Deferred对象(类似快递单号),需要用await获取结果,适合需要聚合数据的场景;launch则像‘寄平邮’,适合日志上报等无需返回值的任务
真题4:协程的挂起函数底层是如何实现的?
面试官:
“你说delay(1000)不阻塞线程,那协程怎么做到‘暂停而不卡死’的?”
候选人:
“这得看Kotlin编译器的‘魔法’——CPS变换和状态机。比如这个挂起函数:
suspend fun fetchData(): String {
delay(1000) // 挂起点1
return "Data" // 挂起点2
}
编译后会变成‘代码乐高’:
Object fetchData(Continuation $completion) {
switch (label) {
case 0:
delay(1000, $completion); // 记录位置1
label = 1;
return COROUTINE_SUSPENDED; // 挂起
case 1:
return "Data"; // 从位置1恢复
}
}
Continuation
就像书签,记录执行到哪里了。当delay
触发时,协程把书签交给线程:‘你先去忙别的,1秒后喊我’。这时候线程腾出手来处理UI点击或者别的请求,完全不卡顿。时间一到,线程通过resume()
把书签插回去,继续执行——整个过程像接力赛,而不是傻站着等。”
真题5:如何用协程优化RecyclerView的图片加载?
面试官:
“列表快速滑动时图片加载卡顿,你们怎么用协程解决的?”
候选人:
“传统方案在onBindViewHolder
里直接开线程,用户一滑到底,几百个请求把线程池挤爆。我们给每个Item绑定独立的Job,滑动时自动取消不可见的请求:
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var loadJob: Job? = null
fun bind(url: String) {
loadJob?.cancel() // 先取消之前的任务
loadJob = lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
loadImage(url) // 耗时操作
}
if (isActive) { // 检查是否已被取消
itemView.image.setImageBitmap(bitmap)
}
}
}
fun unbind() {
loadJob?.cancel() // 视图滚出屏幕时取消
}
}
对比Glide,这套方案更灵活——比如先加载缩略图,再用协程组合高清图加载,还能统一处理异常。之前列表滑动FPS只有30,优化后稳定60,内存波动减少70%。”
Android学习总结之Kotlin 协程_android kotlin协程-CSDN博客https://blog.csdn.net/2301_80329517/article/details/146909197Android学习总结之协程对比优缺点(协程一)_android 协程和线程-CSDN博客
https://blog.csdn.net/2301_80329517/article/details/147256220