第4章 蓝牙扫描流程实现

蓝牙扫描是发现附近蓝牙设备的入口。Android 提供了两套扫描机制:传统蓝牙的 startDiscovery() 和 BLE 的 LeScanCallback / BluetoothLeScanner.startScan()。本章从原理、配置、回调和实际代码层面完整实现蓝牙扫描流程,并解决常见坑点。

4.1 扫描方式对比:startDiscovery vs LE Scan

维度startDiscovery (传统蓝牙)LE Scan (低功耗蓝牙)
目标类型经典蓝牙 (BR/EDR) 设备BLE (Bluetooth Low Energy) 设备
发现方式系统广播 + IntentCallback 回调 + ScanResult 列表
过滤能力无内置过滤ScanFilter 支持 UUID、设备名、MAC 地址过滤
扫描参数不可配置ScanSettings 控制扫描模式、报告延迟等
电量消耗较高低(低延迟模式可调)
兼容性所有 Android 版本API 21+(Android 5.0)

实际开发中,BLE 扫描是主要方向。本课程以 BluetoothLeScanner 为主,同时给出传统蓝牙的备用方案。

4.2 扫描流程概览

核心步骤:获取 BluetoothAdapter → 检查权限 → 配置 ScanFilter + ScanSettings → 启动扫描 → 接收回调 → 更新设备列表。
蓝牙扫描流程 获取 BluetoothAdapter 检查权限 & 蓝牙开启 选择扫描方式 startDiscovery() BluetoothLeScanner 配置 ScanFilter + ScanSettings startScan(filters, settings, callback) BroadcastReceiver 更新设备列表 UI

4.3 ScanFilter 与 ScanSettings 配置

ScanSettings 控制扫描模式(功耗与发现速度的平衡),常见模式:

  • SCAN_MODE_LOW_POWER(0):低功耗,适合后台扫描
  • SCAN_MODE_BALANCED(1):平衡功耗与发现延迟
  • SCAN_MODE_LOW_LATENCY(2):低延迟,高频率扫描,适合快速发现

ScanFilter 可指定一个或多个过滤条件,常用字段:

过滤条件设置方法说明
Service UUIDnew 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() 做保护。