Android-kotlin协程学习总结

发布于:2025-05-31 ⋅ 阅读:(54) ⋅ 点赞:(0)

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,用协程怎么搞?如果有一个接口挂了怎么办?”

候选人​:
“这得请出‘协程三剑客’——asyncawaitcoroutineScope。比如用户主页需要同时拉取用户信息、订单列表和消息通知,可以这么写:

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%。”

面试追问扩展

面试官​:

“协程和线程的本质区别是什么?为什么说协程更适合高并发场景?”

候选人​:

“您可以想象协程就像快递站里的一群快递员,而线程是送货的大卡车。卡车每次出发都要装货、申请路线、排队等调度,一趟只能送一个包裹,成本高还慢。而快递员们共用几辆电动车,一个卡车能塞下几百个快递员,每个人记下自己的送货路线,到了路口灵活切换——协程就这么干的。它不用等操作系统调度,自己管理任务切换,一个线程能跑几万个协程,内存开销只有几十KB。比如我们做秒杀活动,10万人同时抢购,用线程池开500个线程内存就爆了,但换成协程,一个线程轻松扛住,还能自动取消没必要的请求,这就是轻量级的威力。”


面试官​:

“听说GlobalScope容易导致内存泄漏,你们项目里是怎么解决的?”

候选人​:

“这真是血泪教训!之前做视频弹幕功能,用GlobalScope启动了一个无限循环的弹幕请求,结果用户退出页面后,协程还在后台疯狂拉数据,Fragment像‘僵尸’一样赖在内存里,最后直接OOM崩溃。后来才明白,GlobalScope是‘长生不老’的,它的生命周期和整个App绑定,根本不管Activity的死活。现在我们全员改用lifecycleScope——相当于给协程装了智能开关,页面销毁时自动断电。比如在Fragment里发起网络请求,只要用lifecycleScope.launch,用户一返回,请求立刻取消,再也不会出现后台偷偷耗流量的问题了。”


面试官​:

“用户快速滑动商品列表,每个Item都要加载图片,用协程怎么防止卡顿和错乱?”

候选人​:

“这个问题我们优化了三个月!首先,​给每个图片加载任务打标签。比如商品ID是123,加载完成后对比ImageView当前绑定的ID,如果不一样就直接丢弃,防止图片错位。其次,​用协程作用域控制生命周期。在RecyclerView的ViewHolder里,每次绑定新数据时,先取消前一个协程任务,像这样——”

候选人用手比划着空气代码:
“在onBindViewHolder里启动协程前,先检查job是否活跃,如果还在跑就立刻cancel。最后,​给IO操作加限流。比如用Dispatchers.IO的limitedParallelism限制同时加载的图片数,避免100张图同时开线程,线程池直接被打爆。现在我们的列表滑动FPS稳定在60帧,内存占用降了60%。”


面试官​:

“如果协程嵌套了三层异步任务,突然父协程被取消,会发生什么?”

候选人​:

“这就好比拆炸弹时剪错了线——子协程会连锁爆炸!结构化并发的核心思想就是‘要活一起活,要死一起死’。比如父协程负责订单支付,内部启动了子协程A查库存、子协程B扣积分。如果用户突然退出了页面,父协程取消,A和B会立刻收到取消信号,哪怕B已经完成了90%,也会立刻回滚。这虽然残酷,但保证了资源不会部分提交(比如积分扣了但库存没锁)。如果想让某个子协程‘苟活’,比如日志上报任务,可以用SupervisorJob把它包起来,这样即使父协程挂了,它还能在后台默默执行完。”

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