3. Android蓝牙API概览:BluetoothAdapter与BluetoothDevice
做蓝牙开发,说白了就是跟两个类打交道:BluetoothAdapter 和 BluetoothDevice。它们是Android蓝牙框架的左右手,也是后续协程泄漏问题最容易冒头的地方。
我记得刚开始做蓝牙项目时,连这两个家伙的分工都没搞明白,代码写成一锅粥。后来线上出了好几次内存泄漏,才被逼着把API细节啃透。今天咱们就一次性把它们说清楚,顺便埋下协程风险的伏笔。
3.1 BluetoothAdapter——系统蓝牙的「总闸」
BluetoothAdapter 代表手机本地的蓝牙适配器。换句话说,它就是蓝牙硬件在系统里的「代理」。所有开关、扫描、查询已配对设备这些系统级操作,都得通过它来发起。
3.1.1 怎么拿到它?
// 标准获取方式,全程只需调一次
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
// 如果返回 null,说明这台设备压根不支持蓝牙
if (bluetoothAdapter == null) {
// 该提示提示,该降级降级
}
我个人习惯在 Application 或 ViewModel 里把 adapter 缓存下来。因为它是系统单例,整个进程生命周期内不会变,没必要重复获取。
3.1.2 核心方法一览
| 方法 | 功能 | 协程泄漏风险 |
|---|---|---|
| startDiscovery() | 开始扫描周围蓝牙设备 | 高 |
| cancelDiscovery() | 取消正在进行的扫描 | 高(忘调就一直扫) |
| getBondedDevices() | 获取已配对设备列表 | 低(同步调用) |
| listenUsingRfcommWithServiceRecord() | 创建 RFCOMM 服务端 Socket | 中 |
| getRemoteDevice(address) | 通过 MAC 地址创建设备对象 | 低 |
这里我想特别提一下 startDiscovery()。我在项目中遇到过好几次,开发者在协程里启动了扫描,协程因为某些原因取消了,但 cancelDiscovery() 没被调用。结果蓝牙模块一直处于扫描状态,电量肉眼可见地往下掉,用户投诉不断。
3.2 BluetoothDevice——远程设备的「身份证」
BluetoothDevice 代表远端的一个蓝牙设备。它不负责数据通信,只描述设备的身份信息——名字、MAC 地址、是否已配对等。说白了,它就是一张「名片」。
拿到这张名片之后,你才能决定要不要跟它建立连接。
3.2.1 怎么拿到它?
// 方式一:从扫描广播的 Intent 里取
val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
// 方式二:通过已知 MAC 地址直接获取
val device = bluetoothAdapter.getRemoteDevice("00:11:22:33:AA:BB")
// 方式三:从已配对列表里捞
val bondedDevices: Set<BluetoothDevice> = bluetoothAdapter.bondedDevices
3.2.2 核心方法一览
| 方法 | 功能 | 协程泄漏风险 |
|---|---|---|
| createRfcommSocketToServiceRecord() | 创建指向该设备的客户端 Socket | 高 |
| connectGatt() | 建立 BLE 连接 | 高 |
| fetchUuidsWithSdp() | 查询设备支持的服务 UUID | 中 |
| createBond() | 发起配对 | 中 |
connectGatt() 这个方法是重灾区中的重灾区。我见过太多人直接在协程 scope 里调 connectGatt(),然后就不管了。GATT 回调是异步的,协程可能已经取消了,但 BluetoothGattCallback 还在继续跑,导致 Activity 或 Fragment 无法被 GC 回收。
你想想看,用户退出页面了,蓝牙连接还在后台跑着回调,这不是泄漏是什么?
3.3 两个类的关系与协程风险地图
下面这张图把 BluetoothAdapter、BluetoothDevice 以及协程作用域之间的关系画清楚了。红框部分就是最容易泄漏的「高危区」。
从图上可以看到,协程作用域处在应用层,它管理着 BluetoothAdapter 的生命周期。一旦协程被取消,但 BluetoothAdapter 发起的扫描、GATT 连接、Socket 连接没有同步释放,就产生了泄漏。
3.4 避坑指南——我踩过的三个坑
讲完知识,分享几个真实教训。这些都是我曾经在线上环境踩过的坑,每个都跟协程作用域有关。
-
坑一:扫描不停止。 在
viewModelScope.launch里调了startDiscovery(),页面退出了 ViewModel 被 clear,但蓝牙扫描还在跑。后来在onCleared()里补了cancelDiscovery()才解决。 -
坑二:GATT 回调泄漏。 直接把 Activity 作为
BluetoothGattCallback传进去,协程取消了但 Activity 被回调强引用,导致内存泄漏。改成在 ViewModel 里管理回调,并用WeakReference规避。 -
坑三:Socket 挂起不释放。 在协程里用
withContext(Dispatchers.IO)做 RFCOMM 连接,协程取消时 Socket 还在connect()阻塞中。必须用isActive检查状态,配合超时机制。
3.5 本章小结
BluetoothAdapter 和 BluetoothDevice 是 Android 蓝牙开发的起点。前者控制系统级的蓝牙行为,后者代表远端设备身份。两者都涉及大量的异步操作——扫描、连接、配对、服务发现,这些操作一旦与协程作用域绑定,就带来了泄漏风险。
记住一句话:协程可以取消,但蓝牙不会自己停。 后面的章节,我会带你看具体怎么用协程安全地管理这些蓝牙操作,让泄漏无处藏身。