3. Android BLE API:BluetoothGatt、BluetoothGattCallback 等核心类介绍
做 BLE 开发,说白了就是跟几个核心类打交道。你想想看,手机跟蓝牙设备通信,底层走的是 GATT 协议。Google 封装了 BluetoothGatt、BluetoothGattCallback 这些类,方便调用。但坑也埋在这里——没处理好,就成了内存泄漏的重灾区。
我个人习惯,先把这几个类的职责和生命周期理清楚,再去碰那些复杂的连接逻辑。下面咱们一个一个聊。
3.1 BluetoothGatt — 连接的门面
BluetoothGatt 是 BLE 通信的入口。你通过 BluetoothDevice.connectGatt() 拿到它,然后用它发现服务、读写特征值、设置 MTU、控制连接状态。
它的生命周期很简单:连接成功 → 使用 → 关闭。但很多人忘了最后一步:关闭。你在 Activity 销毁或 Fragment 离开时,必须调用 gatt.close(),否则底层资源就悬在那了。
connectGatt() 调用都会生成一个独立的 BluetoothGatt 对象。如果不释放,数量多了直接 OOM。
我记得有个项目,用户连续打开关闭蓝牙页面 20 次,内存就暴涨到 300MB。为什么呢?每打开一次页面,就 new 一个 BluetoothGatt,页面关了却不关 gatt。LeakCanary 一查,全链路上都是这个对象。
3.2 BluetoothGattCallback — 泄漏的“传送带”
BluetoothGattCallback 是 BLE 事件的回调接口。连接状态变化、服务发现完成、特征读写结果,都通过它通知你。这个类本身没有问题,问题在于 它经常持有外部引用。
你写个匿名内部类,不自觉地就 hold 住了外层 Activity 或 Fragment。比如这样:
gatt.connect(new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
// 这里隐式持有外部类引用
if (newState == BluetoothProfile.STATE_CONNECTED) {
mHandler.post(() -> updateUI()); // mHandler 又引用了 Activity
}
}
});
一旦 BluetoothGattCallback 生命周期比外部类长(比如连接断开后系统还延迟了一小段时间才回收),外部类就无法被 GC。这就是典型的 GATT 回调泄漏链:
解决办法?用静态内部类 + 弱引用。或者显式在 onDestroy 里注销回调。我个人偏向用 WeakReference<Context> 包裹外部引用,并且在回调里判空。
3.3 BluetoothDevice — 缓存与复用
BluetoothDevice 代表一个远程蓝牙设备。它本身不直接造成泄漏,但如果你每扫描到一次设备就创建一个新的 BluetoothGatt 对象(用相同的 MAC 地址),系统底层会重用同一个 BluetoothDevice 实例。不过你的 BluetoothGatt 可是新的!
千万别这样干:
for (BluetoothDevice device : scannedDevices) {
BluetoothGatt gatt = device.connectGatt(context, false, callback);
// 每个 device 都 new 一个 gatt,但你可能只需要对一个设备连接一次
}
怎么优化?用 Map<String, BluetoothGatt> 缓存已连接的 GATT 对象,相同 MAC 不再重复创建。我之前在工具类里写过这个缓存,内存占用从 50MB 降到 10MB 以下。
3.4 BluetoothGattService 和 BluetoothGattCharacteristic
这两个是数据载体。服务(Service)包含多个特征值(Characteristic),你发现服务后,要遍历它们。它们生命周期绑定在 BluetoothGatt 上。一旦 gatt.discoverServices() 成功,这些对象就会被创建。如果 BluetoothGatt 泄漏,它们也跟着泄漏。
所以重点始终是:管好 BluetoothGatt 的生命周期。
3.5 核心类关系图
下面这张图总结了你看到的所有类的关系,以及泄漏传播的方向:
3.6 泄漏链核心环节:回调中的上下文引用
现在你知道了,最危险的环节是 BluetoothGattCallback。它内部如果引用了 Activity 或 View,就是一个定时炸弹。我来拆解一下常见的泄漏场景:
- 匿名回调持有外部类 —— 比如在 Activity 里直接 new callback。
- Handler 延迟消息 —— callback 里 post 了一个 Runnable,Runnable 又引用了 View。
- 静态变量缓存 GATT —— 有人图省事,把
BluetoothGatt放在 Application 的静态变量里,导致整个 Activity 泄漏。
onDestroy 里调用 gatt.close() 后,立即把 mGatt = null,再清除 callback 的弱引用。LeakCanary 如果还报泄漏,大概率是其他地方还在引用 callback。
3.7 最佳实践速览
| 核心类 | 正确用法 | 常见泄漏点 |
|---|---|---|
| BluetoothGatt | 用完必须 close(),且只保持一个连接 | 未 close,多连接不清理 |
| BluetoothGattCallback | 静态内部类 + WeakReference<Context> | 匿名内部类持外部引用 |
| BluetoothDevice | 用 MAC 地址做唯一键缓存 | 重复创建 GATT |
| GattService/Characteristic | 随 GATT 自动释放 | 被外部静态集合引用 |