核心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)
}
注意:从 Android 12 开始,扫描蓝牙设备需要 BLUETOOTH_SCAN 权限,并且要在运行时动态申请。光在清单文件里声明是不够的。

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")
    }
}
小技巧:onScanResult 里的 ScanResult 对象,可以通过 result.getDevice().getAddress() 获取 MAC 地址。但 Android 12 以上,非系统应用拿到的可能是随机地址。别太依赖 MAC 地址做设备识别。

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()
核心要点:扫描参数不是越灵敏越好。LOW_LATENCY 模式下,手机蓝牙模块会持续工作,耗电明显。如果你的 App 需要长时间扫描,建议用 LOW_POWER 配合 reportDelay 做批量处理。

把它们串起来:一次完整的扫描

好了,各个零件都介绍完了。咱们把它们组装起来,看看一次完整的扫描长什么样:

// 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),别搞混了。

重要提醒:扫描完成后一定要调用 stopScan。否则蓝牙模块会一直工作,导致手机发热、耗电快。我见过线上版本因为忘记停止扫描,被用户投诉“装了你们的 App 手机半天就没电了”。

知识体系总览

下面这张图,把整个扫描流程的核心关系梳理清楚了。建议你保存下来,写代码时对照着看:

BLE 扫描核心 API 关系图 BluetoothAdapter 获取 BluetoothLeScanner startScan() ScanFilter ScanSettings ScanCallback 回调 ScanResult startScan(filters, settings, callback) 三个参数缺一不可

从图里可以看得很清楚:BluetoothAdapter 是源头,BluetoothLeScanner 是执行者,ScanFilter、ScanSettings、ScanCallback 是三个输入参数。ScanResult 是最终产出。这个流程,你写一百次 BLE 扫描,一百次都是这个套路。

个人习惯:我会把扫描相关的逻辑封装成一个 BLE 扫描管理器,对外暴露 startScan/stopScan 两个方法。内部维护扫描状态和回调引用。这样 Activity 或 Fragment 里只需要调用两行代码,清爽很多。

好了,核心 API 就这些。说白了就是“一个适配器、一个扫描器、三个配置”。你写代码时遇到问题,回头看看这张图,基本能定位到是哪一环出了问题。