常见误区
很多开发者在接触 BLE 扫描时,会产生一个误解:
❌ 使用 ScanFilter 可以让我更快地找到目标设备
实际上:
✅ ScanFilter 不会让扫描更快,但会大幅降低回调频率,节省电量和 CPU
一、ScanFilter 基础用法
1.1 创建基础 Filter
// 最简单的 Filter:只过滤公司 ID
ScanFilter filter = new ScanFilter.Builder()
.setManufacturerData(0x0639, new byte[]{})
.build();
// 应用 Filter
List<ScanFilter> filters = new ArrayList<>();
filters.add(filter);
bluetoothLeScanner.startScan(filters, scanSettings, scanCallback);
1.2 精确 Filter:使用 Mask
// 过滤公司 ID + 特定数据
ScanFilter filter = new ScanFilter.Builder()
.setManufacturerData(
0x0639, // 公司 ID
new byte[]{(byte) 0xCA}, // 数据:帧类型 = 0xCA
new byte[]{(byte) 0xFF} // Mask:完全匹配这个字节
)
.build();
1.3 其他常用 Filter
// 按设备名称过滤
new ScanFilter.Builder()
.setDeviceName("MyDevice")
.build();
// 按 MAC 地址过滤
new ScanFilter.Builder()
.setDeviceAddress("AA:BB:CC:DD:EE:FF")
.build();
// 按 Service UUID 过滤
new ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000fff0-0000-1000-8000-00805f9b34fb"))
.build();
// 组合多个条件
new ScanFilter.Builder()
.setManufacturerData(0x0639, new byte[]{})
.setDeviceName("MyTag")
.build();
二、ScanFilter 的工作原理
2.1 蓝牙扫描的本质
蓝牙芯片 → 接收广播包 → 系统过滤 → App 回调
↓ ↓ ↓ ↓
固定频率 全部收到 应用 Filter 处理数据
关键点:
- 蓝牙芯片的扫描频率是固定的(取决于 ScanSettings)
- Filter 是在系统层面过滤,不是硬件层面
- 发现设备的速度不会因为 Filter 而改变
2.2 有无 Filter 的区别
场景:商场环境,周围有 100 个 BLE 设备
无 Filter:
蓝牙芯片扫描 → 收到 100 个设备广播 → 全部回调到 App
↓
CPU 唤醒 100 次
处理 100 次回调
只有 3 个是目标设备
有 Filter(只过滤公司 ID):
蓝牙芯片扫描 → 收到 100 个设备广播 → 系统过滤 → 只回调 20 个
↓
CPU 唤醒 20 次
处理 20 次回调
3 个是目标设备
有精确 Filter(公司 ID + 帧类型):
蓝牙芯片扫描 → 收到 100 个设备广播 → 系统过滤 → 只回调 3 个
↓
CPU 唤醒 3 次
处理 3 次回调
3 个都是目标设备
三、实战案例:智能设备扫描优化
3.1 场景说明
假设我们要扫描一款智能标签设备,广播数据格式如下:
广播包结构:
[厂商ID: 2字节] [设备类型: 1字节] [固件版本: 1字节] [其他数据...]
具体示例:
厂商ID: 0x0639 (小端序,实际是 0x3906)
设备类型: 0xCA (代表标签类型)
固件版本: 0x01, 0x02, 0x03 等多个版本
3.2 原始代码(无精确过滤)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void initScanner(Context context) {
List<ScanFilter> scanFilters = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 只过滤厂商 ID,满足息屏扫描要求
scanFilters.add(new ScanFilter.Builder()
.setManufacturerData(0x0639, new byte[]{})
.build());
}
// 开始扫描
scanner.startScan(scanFilters, scanSettings, scanCallback);
}
// 回调中需要完整检查所有字段
private boolean isTargetDevice(byte[] scanRecord) {
if (scanRecord == null || scanRecord.length < 10) {
return false;
}
// 解析厂商数据(从广播包中提取)
byte[] manufacturerData = parseManufacturerData(scanRecord);
if (manufacturerData == null || manufacturerData.length < 3) {
return false;
}
// 检查设备类型
byte deviceType = manufacturerData[0];
if (deviceType != (byte) 0xCA) {
return false;
}
// 检查固件版本是否支持
byte firmwareVersion = manufacturerData[1];
return isSupportedVersion(firmwareVersion);
}
3.3 优化后代码(精确过滤)
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void initScanner(Context context) {
List<ScanFilter> scanFilters = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 精确过滤:厂商 ID + 设备类型
scanFilters.add(new ScanFilter.Builder()
.setManufacturerData(
0x0639, // 厂商 ID
new byte[]{(byte) 0xCA}, // 设备类型:标签
new byte[]{(byte) 0xFF} // Mask:完全匹配设备类型
)
.build());
}
scanner.startScan(scanFilters, scanSettings, scanCallback);
}
// 回调中只需检查固件版本
private boolean isTargetDevice(byte[] scanRecord) {
if (scanRecord == null || scanRecord.length < 10) {
return false;
}
byte[] manufacturerData = parseManufacturerData(scanRecord);
if (manufacturerData == null || manufacturerData.length < 3) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 厂商 ID 和设备类型已被 Filter 过滤,直接检查版本
byte firmwareVersion = manufacturerData[1];
return isSupportedVersion(firmwareVersion);
} else {
// 低版本系统走完整检查流程
byte deviceType = manufacturerData[0];
byte firmwareVersion = manufacturerData[1];
return deviceType == (byte) 0xCA && isSupportedVersion(firmwareVersion);
}
}
// 辅助方法:解析厂商数据
private byte[] parseManufacturerData(byte[] scanRecord) {
// 简化示例,实际需要解析广播包结构
// 通常从 scanRecord 中查找厂商数据段
// 返回厂商特定数据部分
return null; // 实际实现省略
}
3.3 性能对比测试
private ScanCallback mScanCallback = new ScanCallback() {
private int totalCount = 0;
private long startTime = System.currentTimeMillis();
@Override
public void onScanResult(int callbackType, final ScanResult result) {
totalCount++;
long elapsed = (System.currentTimeMillis() - startTime) / 1000;
if (totalCount % 10 == 0) {
LogUtil.e("cai_scan", "回调频率: " + totalCount + "次 / " +
elapsed + "秒 = " + (totalCount / Math.max(1, elapsed)) + "次/秒");
}
// 原有处理逻辑...
}
};
3.4 数据对比(非真实数据,需自行对比)
测试环境:办公室,周围约 60 个 BLE 设备,目标设备 3 个
| 方案 | 30秒回调次数 | 回调频率 | CPU唤醒 | 电量消耗 | 发现速度 |
|---|---|---|---|---|---|
| 无 Filter | ~1800次 | 60次/秒 | 高 | 高 | 2秒 |
| 只过滤公司ID | ~600次 | 20次/秒 | 中 | 中 | 2秒 |
| 精确过滤(ID+类型) | ~90次 | 3次/秒 | 低 | 低 | 2秒 |
关键发现:
- ✅ 发现目标设备的时间完全相同(~2秒)
- ✅ 精确过滤将回调次数减少了 95%
- ✅ CPU 唤醒次数减少 95%
- ✅ 电量节省约 60-80%
四、ScanFilter 最佳实践
4.1 什么时候必须使用 Filter?
强制场景(Android 9.0+):
// 息屏/后台扫描必须有 Filter,否则会被系统停止
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 至少要有一个基础 Filter
scanFilter.add(new ScanFilter.Builder()
.setManufacturerData(yourCompanyId, new byte[]{})
.build());
}
4.2 Filter 精确度选择
// 场景1:设备种类单一,只需过滤公司 ID
// 适合:自家产品扫描,环境设备少
new ScanFilter.Builder()
.setManufacturerData(0x0639, new byte[]{})
.build();
// 场景2:多种产品共用公司 ID,需要区分类型
// 适合:同品牌多产品线,商场等密集环境
new ScanFilter.Builder()
.setManufacturerData(
0x0639,
new byte[]{(byte) 0xCA}, // 产品类型标识
new byte[]{(byte) 0xFF}
)
.build();
// 场景3:需要精确匹配多个字段
// 适合:复杂协议,需要硬件级完全过滤
for (String version : supportedVersions) {
byte versionByte = (byte) Integer.parseInt(version, 16);
filters.add(new ScanFilter.Builder()
.setManufacturerData(
0x0639,
new byte[]{(byte) 0xCA, versionByte},
new byte[]{(byte) 0xFF, (byte) 0xFF}
)
.build());
}
4.3 常见错误
错误1:字节序问题
// ❌ 错误:直接使用十六进制值
new ScanFilter.Builder()
.setManufacturerData(0x3906, new byte[]{}) // 错误!
// ✅ 正确:注意小端序
new ScanFilter.Builder()
.setManufacturerData(0x0639, new byte[]{}) // 0x3906 的小端序
错误2:过度优化
// ❌ 不推荐:前台短时扫描不需要过于精确的 Filter
// 增加复杂度,收益很小
if (foregroundScanning && deviceCount < 10) {
// 简单 Filter 就够了
}
// ✅ 推荐:根据场景选择合适的精确度
if (backgroundScanning || deviceCount > 50) {
// 使用精确 Filter
}
错误3:忘记兼容性处理
// ❌ 错误:低版本系统也用 Filter 的假设
private boolean isTargetDevice(byte[] scanRecord) {
// 假设 Filter 已经过滤,直接检查版本
return checkVersion(scanRecord); // 在 Android 8 上会漏检查
}
// ✅ 正确:分版本处理
private boolean isTargetDevice(byte[] scanRecord) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Filter 已过滤公司 ID 和类型
return checkVersion(scanRecord);
} else {
// 完整检查
return checkCompanyId() && checkType() && checkVersion();
}
}
五、总结
核心要点
- ScanFilter 不会让你更快找到设备
-
- 蓝牙扫描频率固定
- 发现时间取决于设备广播间隔
- ScanFilter 的真正价值
-
- 减少无效回调 70-95%
- 降低 CPU 唤醒频率
- 节省电量 60-80%
- 满足 Android 9.0+ 后台扫描要求
- 使用建议
-
- 前台短时扫描:基础 Filter(只过滤公司 ID)
- 后台长时扫描:精确 Filter(多字段匹配)
- 设备密集环境:精确 Filter(减少回调)
- 设备稀疏环境:基础 Filter(简单够用)
一句话总结
ScanFilter 不是为了让你扫得更快,而是为了让 App 在找到目标设备的路上,少做无用功。