1. 课程概述:Coroutine作用域与蓝牙泄漏的背景

做蓝牙开发的朋友,应该都遇到过这种场景——连接连得好好的,突然页面关了,蓝牙却还在后台偷偷跑着。更头疼的是,过一会儿应用就崩了,Logcat里一堆异常。嗯,我早期做蓝牙项目时就被这个问题折磨过好几次。

说白了,这背后藏着一个很典型的问题:Coroutine作用域管理不当导致的蓝牙资源泄漏。这一章我们先来聊聊背景,看看问题到底出在哪儿。

1.1 蓝牙开发中的生命周期痛点

蓝牙通信不像普通的网络请求,发完就完事了。它需要长期维持连接,持续收发数据。这就带来一个核心矛盾:

  • 蓝牙连接:需要长期存活,跨页面、跨组件
  • Android组件:Activity/Fragment 说销毁就销毁

你想想看,当用户退出页面时,Activity 走了 onDestroy,但蓝牙还在收数据。如果没有及时取消协程、关闭连接,会发生什么?

⚠ 典型后果:
  • 协程继续运行,持有 Activity 引用 → 内存泄漏
  • 蓝牙 GATT 连接未释放 → 资源泄漏
  • 系统回收资源时 → 应用崩溃

我个人习惯把这种问题叫作「蓝牙僵尸连接」——表面上看连接还在,实际上已经没人管它了。

1.2 Coroutine作用域到底是什么?

很多新手会把 CoroutineScope 当成一个「启动协程的工具」。其实它的核心作用是管理协程的生命周期。

看个最简单的例子:

// 错误示范:全局作用域
class BleManager {
    private val scope = CoroutineScope(Dispatchers.IO)

    fun connect() {
        scope.launch {
            // 蓝牙连接逻辑
        }
    }
}

这段代码问题在哪儿?scope 用的是全局作用域,永远不会被取消。蓝牙协程会一直活着,哪怕页面已经销毁了。

💡 核心原则: CoroutineScope 的生命周期应该绑定到组件的生命周期。组件销毁时,作用域自动取消,协程自然终止。

1.3 泄漏是怎么一步步发生的?

从「正常连接」到「泄漏崩溃」,其实就几步路:

  1. 建立连接:蓝牙连上了,启动协程做数据交互
  2. 用户退出:Activity 走到 onDestroy
  3. 作用域未取消:协程还在后台跑,持有 View 引用
  4. 资源无法释放:GATT 连接、Socket、文件描述符全卡着
  5. 系统回收:触发 OOM 或 ANR,应用崩溃

我曾经在某个项目中踩过这个坑。那时候刚用协程不久,觉得 launch 就完事了,根本没考虑取消。结果线上反馈说「蓝牙页面退出后再进,越来越卡」。一查内存,好家伙,每个页面泄漏了 3MB 左右。

1.4 一个真实的案例数据

我整理了一下当时项目中的泄漏数据,大家可以看看:

场景 作用域管理 内存占用 GATT 泄漏数 稳定性
页面 A ❌ 未绑定生命周期 泄漏 3.2 MB 2 个 低
页面 B ❌ 手动管理不彻底 泄漏 1.1 MB 1 个 中
页面 C ✅ 使用 LifecycleScope 0 泄漏 0 个 高

看到了吧?就差了那么几行代码,稳定性天差地别。

1.5 课程能帮你解决什么?

这套课程不是为了讲理论,而是实打实地帮你把问题搞定。我会带你:

  • 搞清楚 CoroutineScope、Job、CoroutineContext 在蓝牙场景下的正确用法
  • 掌握 Android 生命周期绑定 的三种常见模式
  • 学会 蓝牙 GATT 连接的正确关闭流程
  • 用 LeakCanary + StrictMode 排查泄漏问题
🎯 一句话总结: 学完这门课,你的蓝牙模块不会再因为「作用域没取消」而泄漏。每个协程都有人管,每个连接都有人关。

嗯,这一章我们就铺垫到这儿。概念清楚了,后面才好动手。

Coroutine作用域与蓝牙泄漏关系图 蓝牙连接建立 创建 CoroutineScope 执行蓝牙数据交互 组件生命周期结束 作用域是否已取消? ✅ 是 ❌ 否 正常取消 → 资源释放 未取消 → 蓝牙泄漏 应用崩溃 / 异常 图例:绿✅ 安全路径 红❌ 泄漏路径

这张图把「正常流程」和「泄漏流程」画得很清楚。左边是安全的——作用域取消了,资源释放了。右边是出事的——作用域没取消,蓝牙泄漏,最终崩溃。

后面的章节,我会针对图中每个节点展开来讲:怎么写代码才能走到左边绿色路径,怎么避免踩进右边红色陷阱。