核心API详解:BluetoothAdapter、BluetoothLeScanner、ScanCallback、ScanFilter、ScanSettings
好,咱们直接进入正题。BLE开发的核心,说白了就是围绕这几个API转。你想想看,手机要跟蓝牙设备通信,第一步是什么?当然是扫描。而扫描这套流程,Android把它封装成了几个关键角色。我刚开始接触时也觉得类有点多,但捋清楚各自的分工后,其实就那么回事。
BluetoothAdapter:一切操作的起点
这个类你应该不陌生。它是整个蓝牙系统的入口。我习惯叫它“蓝牙管家”——开关蓝牙、获取设备、启动扫描,都得通过它。
获取实例的方式很简单:
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
// 设备不支持蓝牙
return
}
这里有个坑,我曾经踩过:getDefaultAdapter() 返回 null 的情况。模拟器上经常这样,真机上极少见,但一定要判空。另外,别忘了检查蓝牙是否开启:
if (!bluetoothAdapter.isEnabled()) {
// 请求用户开启蓝牙
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
BluetoothLeScanner:扫描的执行者
拿到 BluetoothAdapter 后,通过它获取扫描器:
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
嗯,这里要注意:如果蓝牙没打开,bluetoothLeScanner 会返回 null。所以稳妥的做法是:
val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
if (bluetoothLeScanner == null) {
// 处理异常
return
}
我个人习惯把这段逻辑封装成一个工具方法,每次扫描前调用一次,省得重复写判空。
ScanCallback:扫描结果的处理者
扫描结果怎么回来?全靠这个回调。它有三个方法:
| 方法 | 触发时机 | 说明 |
|---|---|---|
| onScanResult | 扫描到单个设备 | 最常用,每次发现设备都会回调 |
| onBatchScanResults | 批量结果返回 | 配合 ScanSettings 的延时模式使用 |
| onScanFailed | 扫描失败 | 错误码告诉你为什么失败 |
实际开发中,我几乎只用 onScanResult。批量扫描在低功耗场景下有用,但调试起来比较麻烦。来看一个典型实现:
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
val device = it.device
val rssi = it.rssi
val scanRecord = it.scanRecord
// 处理设备信息
Log.d("BLE", "发现设备: ${device.name}, RSSI: $rssi")
}
}
override fun onScanFailed(errorCode: Int) {
// 错误码 1 表示扫描已开始但被中断
// 错误码 2 表示内部错误
// 错误码 3 表示参数无效
// 错误码 4 表示蓝牙未开启
Log.e("BLE", "扫描失败,错误码: $errorCode")
}
}
ScanFilter:精准扫描的过滤器
不设过滤的话,周围所有蓝牙设备都会扫出来。你想想看,办公室里几十个蓝牙设备,列表刷得飞起,根本找不到你要的那个。这时候 ScanFilter 就派上用场了。
常用的过滤方式有几种:
- 按服务 UUID 过滤:最常用,每个 BLE 设备都有自己独特的 UUID
- 按设备名称过滤:简单粗暴,但设备名可能重复
- 按 MAC 地址过滤:精确但不够灵活
- 按制造商数据过滤:高级用法,特定厂商的设备会用
我项目中用得最多的是按 UUID 过滤。举个例子:
val filter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"))
.build()
这里有个细节:UUID 的大小写不敏感,但建议统一用小写。我曾经因为 UUID 里混了大写字母,排查了半天才发现是格式问题。
多个过滤条件可以组合:
val filters = listOf(
ScanFilter.Builder().setServiceUuid(uuid1).build(),
ScanFilter.Builder().setServiceUuid(uuid2).build()
)
这样扫描时,只要匹配任意一个过滤条件的设备都会被回调。
ScanSettings:扫描行为的配置器
扫描的功耗、频率、延迟,都由这个类控制。说白了,就是告诉系统:你是要快还是要省电?
核心参数就三个:
| 参数 | 可选值 | 说明 |
|---|---|---|
| scanMode | SCAN_MODE_LOW_POWER SCAN_MODE_BALANCED SCAN_MODE_LOW_LATENCY |
低功耗/平衡/低延迟,功耗和速度的权衡 |
| callbackType | CALLBACK_TYPE_ALL_MATCHES CALLBACK_TYPE_FIRST_MATCH CALLBACK_TYPE_MATCH_LOST |
回调触发时机 |
| reportDelay | 0 或大于 0 的毫秒数 | 0 表示实时回调,大于 0 表示批量延迟上报 |
我个人的经验是:调试阶段用 LOW_LATENCY,响应快,方便验证逻辑。发布到生产环境后,根据场景切换到 BALANCED 或 LOW_POWER。省电效果很明显。
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setReportDelay(0)
.build()
把它们串起来:一次完整的扫描
好了,各个零件都介绍完了。咱们把它们组装起来,看看一次完整的扫描长什么样:
// 1. 获取适配器
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() ?: return
if (!bluetoothAdapter.isEnabled()) return
// 2. 获取扫描器
val scanner = bluetoothAdapter.bluetoothLeScanner ?: return
// 3. 配置过滤和设置
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"))
.build()
)
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.build()
// 4. 开始扫描
scanner.startScan(filters, settings, scanCallback)
// 5. 停止扫描(通常在 onPause 或不需要时调用)
// scanner.stopScan(scanCallback)
这里有个顺序问题:先配置再启动。我见过有人把 filters 和 settings 传反了,结果扫描一直没反应。startScan 的方法签名是 (filters, settings, callback),别搞混了。
知识体系总览
下面这张图,把整个扫描流程的核心关系梳理清楚了。建议你保存下来,写代码时对照着看:
从图里可以看得很清楚:BluetoothAdapter 是源头,BluetoothLeScanner 是执行者,ScanFilter、ScanSettings、ScanCallback 是三个输入参数。ScanResult 是最终产出。这个流程,你写一百次 BLE 扫描,一百次都是这个套路。
好了,核心 API 就这些。说白了就是“一个适配器、一个扫描器、三个配置”。你写代码时遇到问题,回头看看这张图,基本能定位到是哪一环出了问题。