第4章 蓝牙扫描流程实现
蓝牙扫描是发现附近蓝牙设备的入口。Android 提供了两套扫描机制:传统蓝牙的 startDiscovery() 和 BLE 的 LeScanCallback / BluetoothLeScanner.startScan()。本章从原理、配置、回调和实际代码层面完整实现蓝牙扫描流程,并解决常见坑点。
4.1 扫描方式对比:startDiscovery vs LE Scan
| 维度 | startDiscovery (传统蓝牙) | LE Scan (低功耗蓝牙) |
|---|---|---|
| 目标类型 | 经典蓝牙 (BR/EDR) 设备 | BLE (Bluetooth Low Energy) 设备 |
| 发现方式 | 系统广播 + Intent | Callback 回调 + ScanResult 列表 |
| 过滤能力 | 无内置过滤 | ScanFilter 支持 UUID、设备名、MAC 地址过滤 |
| 扫描参数 | 不可配置 | ScanSettings 控制扫描模式、报告延迟等 |
| 电量消耗 | 较高 | 低(低延迟模式可调) |
| 兼容性 | 所有 Android 版本 | API 21+(Android 5.0) |
实际开发中,BLE 扫描是主要方向。本课程以 BluetoothLeScanner 为主,同时给出传统蓝牙的备用方案。
4.2 扫描流程概览
核心步骤:获取 BluetoothAdapter → 检查权限 → 配置 ScanFilter + ScanSettings → 启动扫描 → 接收回调 → 更新设备列表。
4.3 ScanFilter 与 ScanSettings 配置
ScanSettings 控制扫描模式(功耗与发现速度的平衡),常见模式:
SCAN_MODE_LOW_POWER(0):低功耗,适合后台扫描SCAN_MODE_BALANCED(1):平衡功耗与发现延迟SCAN_MODE_LOW_LATENCY(2):低延迟,高频率扫描,适合快速发现
ScanFilter 可指定一个或多个过滤条件,常用字段:
| 过滤条件 | 设置方法 | 说明 |
|---|---|---|
| Service UUID | new ScanFilter.Builder().setServiceUuid(uuid) | 精确匹配 16/32/128 位 UUID |
| 设备名称 | setDeviceName("myDevice") | 模糊匹配(部分名称也可触发) |
| MAC 地址 | setDeviceAddress("AA:BB:CC:DD:EE:FF") | 精确匹配 |
| Manufacturer 数据 | setManufacturerData(companyId, data) | 按制造商 ID 与数据前缀过滤 |
配置示例:
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setReportDelay(0) // 实时回调
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 默认
.build();
ScanFilter filter = new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"))
.build();
List<ScanFilter> filters = new ArrayList<>();
filters.add(filter);
提示:如果不需要过滤,
filters 可以传空列表,但 Android 11+ 上不传过滤器可能导致部分扫描回调被限制。建议至少添加一个宽松的 ScanFilter(如匹配任何设备)。
4.4 扫描回调与设备列表更新
LE 扫描回调通过 ScanCallback 接收结果:
private final ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
int rssi = result.getRssi();
ScanRecord scanRecord = result.getScanRecord();
// 更新设备列表(去重 + 刷新信号强度)
updateDeviceList(device, rssi, scanRecord);
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
// 批量结果(当 setReportDelay > 0 时触发)
for (ScanResult result : results) {
onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result);
}
}
@Override
public void onScanFailed(int errorCode) {
// 错误码:SCAN_FAILED_ALREADY_STARTED 等
Log.e("BLE", "Scan failed, error: " + errorCode);
}
};
设备列表更新通常使用 Map<String, DeviceInfo> 按 MAC 去重:
Map<String, DeviceInfo> deviceMap = new HashMap<>();
private void updateDeviceList(BluetoothDevice device, int rssi, ScanRecord record) {
String address = device.getAddress();
DeviceInfo info = deviceMap.get(address);
if (info == null) {
info = new DeviceInfo(device.getName(), address, rssi, record);
deviceMap.put(address, info);
} else {
info.setRssi(rssi); // 更新信号强度
info.setScanRecord(record);
}
// 通知 RecyclerView 刷新
runOnUiThread(() -> adapter.notifyDataSetChanged());
}
注意事项:
onScanResult 在后台线程回调,切勿直接更新 UI,必须切换到主线程。 Android 10 之后,Wi-Fi 扫描与蓝牙扫描存在调度优化,可能延迟回调。
4.5 调试与常见错误
| 错误码/现象 | 常见原因 | 解决方案 |
|---|---|---|
SCAN_FAILED_ALREADY_STARTED (1) | 重复调用 startScan 未停止 | 先调用 stopScan(callback) |
SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) | 应用注册失败,通常因权限不足 | 确保 ACCESS_FINE_LOCATION 已授予(Android 10+) |
SCAN_FAILED_INTERNAL_ERROR (3) | 蓝牙内部错误 | 重启蓝牙或检查硬件 |
| 扫描不到任何设备 | 蓝牙未开启 / 过滤器过严 / 权限缺失 | 检查蓝牙状态,放宽过滤器,检查 Manifest |
| 设备重复出现 | 未去重 | 使用 MAC 地址作为唯一键 |
4.6 性能与稳定性优化
- 及时停止扫描:发现目标设备后立即调用
stopScan(),避免电量浪费。 - 使用合适的扫描模式:前台使用
LOW_LATENCY,后台使用LOW_POWER。 - 控制回调频率:设置
setReportDelay(如 100ms)收集批量结果,减少 UI 刷新次数。 - 避免在主线程执行:回调内部不要做耗时的 JSON 解析或数据库写入。
- Android 12+ 新限制:后台扫描需
BLUETOOTH_SCAN权限,且foregroundServiceType声明connectedDevice。
实际部署时,建议在 Application 类中统一管理扫描生命周期,并配合 BluetoothAdapter.getDefaultAdapter().isEnabled() 做保护。