为什么你的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章,我按"认知-发现-解决-预防"四个阶段来安排的。先给你看个总图:
课程简介与目标
这门课不是讲协程API怎么用的。那种课网上太多了。
咱们的目标很具体:当你面临一个实际BLE项目,出现内存泄漏或逻辑混乱时,你能用协程相关的知识,在30分钟内定位到根因,并给出稳定的修复方案。
| 课程目标 | 说明 |
|---|---|
| 理解泄漏本质 | 协程泄漏 ≠ 线程泄漏,它是 Continuation 引用链导致的 GC Root 不可达 |
| 掌握定位方法 | 会用 Android Studio Profiler、MAT 等工具找出泄漏链 |
| 学会解决模式 | 从 callback + launch 到 flow + callbackFlow,再到 structured concurrency |
| 能写防泄漏代码 | 把生命周期感知做到代码层面,而不是靠"小心" |
前置知识与学习路径
我建议按这个顺序来消化内容:
- 先看第1-4章,理解"为什么泄漏"——这部分是认知地基。
- 接着第5-12章是实战定位,我会带着你用工具反复验证。
- 第13-22章是解决方案,每个方案我都给了真实项目中的对比数据。
- 最后第23-30章是工程化预防,包括单元测试、CI检查、代码审查清单。
我的习惯:每学完一个章节,回到自己的项目里找一个类似的回调(不一定是BLE,也可能是网络回调、传感器回调),试着用课里的思路分析一下是否有泄漏隐患。边学边用记得最牢。
你可能会问:协程泄漏怎么比线程泄漏还可怕?
线程泄漏好歹会报异常、堆栈能看。协程泄漏呢?它静悄悄的。你看 logcat 没错误,内存也还在涨——只是涨得慢。等到 OOM 了,你可能已经提审了新版本。
我曾经在一个项目里,发现 onCharacteristicChanged 回调里启动了一个协程做数据库写入。用户操作频繁时,一次回调没结束,新的又来了。协程排着队,每个都持有 DB 连接的引用。半小时后,内存占用从 60MB 涨到 200MB。
而且关键是,你以为 coroutineScope 在 Activity onDestroy 时取消了?错——那个 scope 如果被回调的匿名内部类隐式持有了,它根本不知道 Activity 死了。
所以接下来,咱们就要彻底搞清楚这背后的引用链。