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 缓存下来。因为它是系统单例,整个进程生命周期内不会变,没必要重复获取。

注意: getDefaultAdapter() 真的可能返回 null!模拟器、某些车载系统、TV 设备经常不带蓝牙。不判空直接调用方法,一秒崩给你看。

3.1.2 核心方法一览

方法 功能 协程泄漏风险
startDiscovery() 开始扫描周围蓝牙设备 高
cancelDiscovery() 取消正在进行的扫描 高(忘调就一直扫)
getBondedDevices() 获取已配对设备列表 低(同步调用)
listenUsingRfcommWithServiceRecord() 创建 RFCOMM 服务端 Socket 中
getRemoteDevice(address) 通过 MAC 地址创建设备对象 低

这里我想特别提一下 startDiscovery()。我在项目中遇到过好几次,开发者在协程里启动了扫描,协程因为某些原因取消了,但 cancelDiscovery() 没被调用。结果蓝牙模块一直处于扫描状态,电量肉眼可见地往下掉,用户投诉不断。

小习惯:我每次写 startDiscovery() 都会在协程的 finally 块或者 CoroutineContext 的取消回调里补上 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
注意: getRemoteDevice() 不会真的去连接设备。它只是在本地 new 了一个「代理对象」。真正的网络通信在后面。

3.2.2 核心方法一览

方法 功能 协程泄漏风险
createRfcommSocketToServiceRecord() 创建指向该设备的客户端 Socket 高
connectGatt() 建立 BLE 连接 高
fetchUuidsWithSdp() 查询设备支持的服务 UUID 中
createBond() 发起配对 中

connectGatt() 这个方法是重灾区中的重灾区。我见过太多人直接在协程 scope 里调 connectGatt(),然后就不管了。GATT 回调是异步的,协程可能已经取消了,但 BluetoothGattCallback 还在继续跑,导致 Activity 或 Fragment 无法被 GC 回收。

你想想看,用户退出页面了,蓝牙连接还在后台跑着回调,这不是泄漏是什么?

3.3 两个类的关系与协程风险地图

下面这张图把 BluetoothAdapter、BluetoothDevice 以及协程作用域之间的关系画清楚了。红框部分就是最容易泄漏的「高危区」。

Android 蓝牙核心 API 关系与协程风险点 ▸ 应用层 ▸ 系统层 ▸ 设备层 协程作用域 BluetoothAdapter 蓝牙系统服务 BluetoothDevice GATT 连接 管理生命周期 IPC 调用 发现 / 获取 系统层管理设备状态 connectGatt() ⚠️ 高危区 协程取消时 需释放资源

从图上可以看到,协程作用域处在应用层,它管理着 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 检查状态,配合超时机制。
核心原则:蓝牙操作往往比协程的生命周期「长」。协程取消了,蓝牙任务不会自动停掉。你必须手动在取消回调或 finally 块里做清理。

3.5 本章小结

BluetoothAdapter 和 BluetoothDevice 是 Android 蓝牙开发的起点。前者控制系统级的蓝牙行为,后者代表远端设备身份。两者都涉及大量的异步操作——扫描、连接、配对、服务发现,这些操作一旦与协程作用域绑定,就带来了泄漏风险。

记住一句话:协程可以取消,但蓝牙不会自己停。 后面的章节,我会带你看具体怎么用协程安全地管理这些蓝牙操作,让泄漏无处藏身。