为什么你的BLE回调里藏着协程泄漏

做蓝牙开发的兄弟,有没有遇到过这种情况——

App连上设备,收了几包数据,一切正常。切到后台再切回来,发现回调不走了。或者连了几次之后,内存蹭蹭往上涨,GC都拉不回来。

我以前也以为是蓝牙协议栈的锅,查了三天才发现:是协程泄漏了。而且泄漏点就藏在咱们最常见的BLE回调里。

这节课,咱们就聊聊这个痛点。为什么协程会泄漏?怎么定位?怎么根治?

BLE回调的"坑"到底在哪

Android BLE通信,核心是基于回调的。你注册一个 BluetoothGattCallback,系统在收到数据、连接状态变化时回调你。

val gattCallback = object : BluetoothGattCallback() {
    override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
        // 这里我们启动了协程处理数据
        scope.launch {
            processData(characteristic.value)
        }
    }
}

看起来没什么问题对吧?

嗯,陷阱就在这里。你觉得 scope 是谁的?它什么时候取消?如果蓝牙断开后,系统还继续回调呢?

注意:BLE 回调可能在任何线程被调用,甚至是在 Gatt 对象已经失效之后。如果协程 scope 没有被正确管理,协程会一直存在,直到 scope 被显式取消或 JVM 回收。而 scope 的持有者——比如 Activity 或 Fragment——可能已经被销毁了。

我当年踩过的坑

我记得第一次遇到这个问题,是在一个智能手环项目里。用户反馈:连续连接10次后,App就卡死了。

我一开始怀疑是 Gatt 对象太多,做了一层池化管理。结果发现,即使只连一次,长时间操作后也会泄漏。打了一个 heap dump 一看——好家伙,Continuation 对象成堆地挂在 BLE 回调的调用链上。

所有协程的挂起函数底层都是 Continuation。一个回调启动一个协程,如果这个协程因为某些原因没结束,而回调又被反复调用,那就会积累大量未完成的协程。它们引用着外部对象,导致内存泄漏。

说白了,这就是个典型的生命周期错配问题。

课程整体路径一览

咱们这门课一共30章,我按"认知-发现-解决-预防"四个阶段来安排的。先给你看个总图:

课程知识体系:Kotlin协程在BLE回调中泄漏定位实战 阶段1:认知 协程泄漏原理 BLE回调生命周期 典型泄漏场景 阶段2:发现 Heap Dump分析 协程栈追踪 泄漏检测工具 阶段3:解决 Scope绑定策略 Callback转Flow 结构化并发改造 阶段4:预防 单元测试 静态分析 代码规范 ▶ 前置知识(建议先掌握) • Kotlin 基础语法(特别是 lambda 和扩展函数) • 协程基础:launch / async / suspend / Dispatchers • Android BLE 基本流程:Gatt 连接、发现服务、通知启用 • Android 生命周期:Activity / Fragment 的 onDestroy • 如果上述某项不熟,建议先花半天补一下,再回来听效果更好

课程简介与目标

这门课不是讲协程API怎么用的。那种课网上太多了。

咱们的目标很具体:当你面临一个实际BLE项目,出现内存泄漏或逻辑混乱时,你能用协程相关的知识,在30分钟内定位到根因,并给出稳定的修复方案。

课程目标 说明
理解泄漏本质 协程泄漏 ≠ 线程泄漏,它是 Continuation 引用链导致的 GC Root 不可达
掌握定位方法 会用 Android Studio Profiler、MAT 等工具找出泄漏链
学会解决模式 从 callback + launch 到 flow + callbackFlow,再到 structured concurrency
能写防泄漏代码 把生命周期感知做到代码层面,而不是靠"小心"

前置知识与学习路径

我建议按这个顺序来消化内容:

  1. 先看第1-4章,理解"为什么泄漏"——这部分是认知地基。
  2. 接着第5-12章是实战定位,我会带着你用工具反复验证。
  3. 第13-22章是解决方案,每个方案我都给了真实项目中的对比数据。
  4. 最后第23-30章是工程化预防,包括单元测试、CI检查、代码审查清单。

我的习惯:每学完一个章节,回到自己的项目里找一个类似的回调(不一定是BLE,也可能是网络回调、传感器回调),试着用课里的思路分析一下是否有泄漏隐患。边学边用记得最牢。

你可能会问:协程泄漏怎么比线程泄漏还可怕?

线程泄漏好歹会报异常、堆栈能看。协程泄漏呢?它静悄悄的。你看 logcat 没错误,内存也还在涨——只是涨得慢。等到 OOM 了,你可能已经提审了新版本。

我曾经在一个项目里,发现 onCharacteristicChanged 回调里启动了一个协程做数据库写入。用户操作频繁时,一次回调没结束,新的又来了。协程排着队,每个都持有 DB 连接的引用。半小时后,内存占用从 60MB 涨到 200MB。

而且关键是,你以为 coroutineScope 在 Activity onDestroy 时取消了?错——那个 scope 如果被回调的匿名内部类隐式持有了,它根本不知道 Activity 死了。

所以接下来,咱们就要彻底搞清楚这背后的引用链。