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 回调泄漏链:

我曾经踩过的坑: 在 Service 里做长时间扫描 + 连接,用了匿名 callback。Service 停止后,Binder 还活着,callback 一直没被释放。最后 OOM 崩溃。排查了两天才发现是 callback 持有 Service 引用。

解决办法?用静态内部类 + 弱引用。或者显式在 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 核心类关系图

下面这张图总结了你看到的所有类的关系,以及泄漏传播的方向:

Android BLE 核心类关系与泄漏链 Activity / Fragment / Service BluetoothGatt BluetoothGattCallback (持有外部引用 → 泄漏) BluetoothDevice BluetoothGattService
BluetoothGattCharacteristic 泄漏链:App → BluetoothGatt → BluetoothGattCallback → App (循环引用) 解决:使用静态内部类 + 弱引用,并在 onDestroy 中 gatt.close() 和移除回调

3.6 泄漏链核心环节:回调中的上下文引用

现在你知道了,最危险的环节是 BluetoothGattCallback。它内部如果引用了 Activity 或 View,就是一个定时炸弹。我来拆解一下常见的泄漏场景:

  1. 匿名回调持有外部类 —— 比如在 Activity 里直接 new callback。
  2. Handler 延迟消息 —— callback 里 post 了一个 Runnable,Runnable 又引用了 View。
  3. 静态变量缓存 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 自动释放 被外部静态集合引用