Kotlin 协程实战避坑:90% 的人都会踩的生命周期泄漏问题

Kotlin 协程实战避坑:90% 的人都会踩的生命周期泄漏问题

协程很轻,但"轻"不代表不会泄漏。

真正出事的不是协程本身,而是:协程比启动它的页面/组件活得更久


一、先说结论:协程泄漏的本质

协程泄漏不是"内存一下子爆了",而是这几种慢性中毒:

  • Activity / Fragment 销毁了,协程还在跑
  • 网络结果回来后更新已经销毁的 View → crash
  • ViewModel 里持有 Activity / Context 强引用
  • Flow 一直在收集,界面都不可见了还在消费数据
  • 自定义 Scope 忘了 cancel(),僵尸协程越积越多

一句话:

没有"父作用域"管着的协程,迟早变成泄漏。


二、最经典的坑:GlobalScope.launch

ini 复制代码
// ❌ 生产环境最常见反面教材
GlobalScope.launch {
    val user = api.getUser()
    textView.text = user.name   // Activity 都没了你还更新?
}

GlobalScope 跟 App 进程同寿:

  • 不随 Activity 销毁
  • 不随 Fragment 销毁
  • 不随 ViewModel 清除
  • 协程里拿了 this@Activity,Activity 就 GC 不掉

后果:

  • 内存占用悄悄涨
  • 回包后 IllegalStateException: Fragment not attached
  • 老用户数据覆盖新用户数据(金融/聊天场景尤其致命)

✅ 正确姿势:

ini 复制代码
// Activity / Fragment 里
lifecycleScope.launch {
    val user = api.getUser()
    textView.text = user.name
}

// ViewModel 里
viewModelScope.launch {
    _uiState.value = uiState.copy(user = api.getUser())
}

三、viewModelScope ≠ lifecycleScope,用错也会"逻辑泄漏"

很多人以为:"都用生命周期 Scope 就安全了。"

选错 Scope,一样出问题

1. viewModelScope

  • 归属:ViewModel

  • 销毁时机:ViewModel.onCleared()

  • 抗旋转:✅ 屏幕旋转不会取消

  • 适合:

    • 网络请求
    • 数据库读写
    • 业务状态计算
kotlin 复制代码
class UserViewModel : ViewModel() {
    fun loadUser(id: String) {
        viewModelScope.launch {
            _state.value = State.Loading
            _state.value = State.Success(repo.getUser(id))
        }
    }
}

2. lifecycleScope

  • 归属:Activity / Fragment

  • 销毁时机:组件 DESTROYED

  • 旋转会重建 → 协程也会被取消重启

  • 适合:

    • 一次性 UI 操作
    • 弹窗、动画、临时任务

3. Fragment 里还有一个隐藏坑:viewLifecycleOwner

arduino 复制代码
// ❌ 用 Activity 的 lifecycle 收集 Flow
lifecycleScope.launch {
    viewModel.userFlow.collect { ... }
}

Fragment 里 View 没了,但 Fragment 本身还没死,这时候更新 View 就会炸。

✅ 正确:

ini 复制代码
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.userFlow.collect {
            binding.tvName.text = it.name
        }
    }
}

ViewModel 出状态,UI 用 repeatOnLifecycle 收状态。


四、Flow 是重灾区:launchIn(GlobalScope) 直接劝退

arduino 复制代码
// ❌ 永远别这么写
viewModel.userFlow
    .onEach { textView.text = it.name }
    .launchIn(GlobalScope)

正确写法二选一:

写法 1:Fragment / Activity

scss 复制代码
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.userFlow.collect {
            render(it)
        }
    }
}

写法 2:ViewModel 里绑定自己的 Scope

kotlin 复制代码
fun observeUser() {
    userFlow
        .onEach { _uiState.value = it }
        .launchIn(viewModelScope)
}

五、ViewModel 持有关键引用:更隐蔽的泄漏

kotlin 复制代码
class MyViewModel : ViewModel() {
    var activity: AppCompatActivity? = null   // ❌ 大坑

    fun doWork() {
        viewModelScope.launch {
            delay(10_000)
            activity?.showToast("完成") // Activity 早死了
        }
    }
}

ViewModel 存活时间长于 Activity,

一旦它拿着 Activity / View / Context,GC 直接失效。

✅ 正确做法:

  • ViewModel 只持有数据
  • UI 状态用 StateFlow / LiveData
  • 需要 Context?传 Application 或用系统服务
  • 回调?用事件总线 / Channel / 单向数据流

六、自定义 CoroutineScope:用得好是架构,用不好是炸弹

有时候你确实需要自定义 Scope,比如:

  • 应用级同步
  • 登录态刷新
  • 离线队列
  • 长连接心跳

❌ 错误写法

kotlin 复制代码
class SyncManager {
    private val scope = CoroutineScope(Dispatchers.IO)

    fun start() {
        scope.launch { syncLoop() }
    }
    // 没人 cancel
}

✅ 正确写法

kotlin 复制代码
class SyncManager(
    private val applicationScope: CoroutineScope // 明确所有者
) {
    fun start() {
        applicationScope.launch {
            syncLoop()
        }
    }
}

如果是自己建的 Scope:

kotlin 复制代码
class MyActivity : AppCompatActivity() {
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.Main + job)

    override fun onDestroy() {
        super.onDestroy()
        job.cancel() // ✅ 必须
    }
}

经验法则:

自己 new 的 Scope,自己负责埋单。


七、协程"取消了",但代码还在跑?

Kotlin 协程是协作式取消 ,不是线程 stop()

scss 复制代码
viewModelScope.launch {
    while (true) {          // ❌ 不检查取消
        doHeavyWork()
    }
}

取消信号只会在挂起点生效。

✅ 正确:

scss 复制代码
viewModelScope.launch {
    while (isActive) {
        doHeavyWork()
        delay(1000)
    }
}

或者:

kotlin 复制代码
suspend fun work() {
    ensureActive()
    doSomething()
}

⚠️ 千万别这么写:

php 复制代码
try {
    ...
} catch (e: Exception) {
    // 吞掉 CancellationException ❌
}

应该是:

php 复制代码
} catch (e: CancellationException) {
    throw e
} catch (e: Exception) {
    // 真正的业务异常
}

八、有些工作"不该被协程管":别硬撑

下面这些需求,协程本来就不适合

  • 退出页面也要上传完
  • App 被杀也要继续同步
  • 登录后必须跑完的离线队列
  • 大文件上传 / 下载

这时候别再想 Scope 了,直接用:

  • WorkManager
  • ForegroundService + Worker
  • 本地任务队列 + 启动时补跑

协程是"进程内并发工具",不是"持久化任务系统"。


九、一张速查表(面试/Code Review 都能用)

场景 该用哪个
Activity / Fragment 里临时 UI 任务 lifecycleScope
Fragment 里收集 Flow viewLifecycleOwner + repeatOnLifecycle
ViewModel 里请求/状态/业务逻辑 viewModelScope
Compose 里点击触发协程 rememberCoroutineScope()
应用级后台任务 自定义 Application Scope / WorkManager
退出页面也要完成 WorkManager / 任务队列
图省事全局跑 ❌ GlobalScope

十、防泄漏 Checklist

launch 前问自己三个问题:

  1. 这个协程归谁所有?
  2. 组件死了它该不该死?
  3. 它有没有拿 Activity / View / Context?

Code Review 再加三条:

  • ❌ 禁止 GlobalScope
  • ❌ 禁止 Repository 里自己 CoroutineScope().launch
  • ✅ 所有 Flow 收集都有生命周期边界

最后一句人话总结

协程不是"开了就完事",

每一个 launch,都必须有一个会死掉的爸爸。

没有 Scope 的协程,是野孩子;

有 Scope 但不对的协程,是定时炸弹;

有生命周期、有状态边界、尊重取消的协程,才是生产级代码。

相关推荐
苏三说技术1 小时前
Spring AI、LangChain4j、AgentScope、Embabel,哪个AI框架更好?
后端
烽学长2 小时前
(附源码)基于Springboot+vue的图书阅读分享系统的设计与实现
java·spring boot·后端
isfox2 小时前
Python 单例模式:一个类只能有一个实例,到底怎么实现?
后端
青山木2 小时前
RocketMQ 入门到原理(三):消息存储原理
java·后端·中间件·架构·rocketmq
小白男神2 小时前
MySQL进阶学习三(视图)
后端·mysql
猿人谷2 小时前
Jev:当 AI 不再生成 Token,而是直接做决策
后端·langchain·aigc
苍何2 小时前
WorkBuddy + 腾讯乐享,原来知识库还能这么用
后端
站大爷IP2 小时前
Python的生成器把我坑惨了,原来yield和return的区别这么大
后端