ScanFilter 完全解析:不是为了更快,而是为了更省

44 阅读6分钟

常见误区

很多开发者在接触 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();
    }
}

五、总结

核心要点

  1. ScanFilter 不会让你更快找到设备
    • 蓝牙扫描频率固定
    • 发现时间取决于设备广播间隔
  1. ScanFilter 的真正价值
    • 减少无效回调 70-95%
    • 降低 CPU 唤醒频率
    • 节省电量 60-80%
    • 满足 Android 9.0+ 后台扫描要求
  1. 使用建议
    • 前台短时扫描:基础 Filter(只过滤公司 ID)
    • 后台长时扫描:精确 Filter(多字段匹配)
    • 设备密集环境:精确 Filter(减少回调)
    • 设备稀疏环境:基础 Filter(简单够用)

一句话总结

ScanFilter 不是为了让你扫得更快,而是为了让 App 在找到目标设备的路上,少做无用功。