11、蓝牙低功耗扫描优化:扫描参数调优、过滤器、线程管理与省电策略

BLE扫描,说白了就是手机在不停地“听”周围有没有设备在广播。但这里有个矛盾——你听得越勤快,耗电越快;你听得越仔细,CPU负担越重。我刚开始做BLE开发时,就踩过这个坑:扫描写得太糙,结果手机发烫,用户直接给了一星差评。

所以这一章,咱们就聊聊怎么把扫描这件事做得既高效又省电。我会从四个维度展开:扫描参数怎么调、过滤器怎么用、回调线程怎么管、以及省电策略怎么落地。

11.1 扫描参数调优:scanMode 与 reportDelay

先看两个最基础的参数。它们直接影响扫描的功耗和实时性。

11.1.1 scanMode:扫描模式的选择

Android 提供了三种扫描模式,定义在 ScanSettings 里:

模式 常量值 扫描间隔 适用场景
低功耗模式 SCAN_MODE_LOW_POWER 约 400ms 后台扫描、设备不在附近时
平衡模式 SCAN_MODE_BALANCED 约 200ms 一般性发现,折中方案
低延迟模式 SCAN_MODE_LOW_LATENCY 约 100ms 需要快速发现设备时

我个人习惯是:不要一上来就用低延迟模式。你想想看,如果用户只是打开页面等设备列表刷新,平衡模式完全够用。低延迟模式留给“正在配对”或“正在寻找特定设备”的场景。

核心原则:扫描模式越激进,功耗越高。请根据业务场景动态切换,而不是写死一个值。

11.1.2 reportDelay:报告延迟

这个参数很多人会忽略。它控制的是扫描结果回调的时机。

  • reportDelay = 0:立即回调,每收到一个广播包就触发一次。
  • reportDelay > 0:延迟上报,系统会把一段时间内的扫描结果批量返回。

我在项目中遇到过一个问题:扫描回调太频繁,导致 UI 线程卡顿。后来我把 reportDelay 设为 100ms,效果立竿见影。CPU 占用率直接降了 30%。

小技巧:如果你只是做设备列表展示,设 100-200ms 的延迟就够了。既保证实时性,又减少回调次数。

11.2 扫描过滤器:ScanFilter 的正确用法

不设过滤器的扫描,就像在大街上随便喊“谁在?”——所有设备都会回应你。这既浪费电,又增加处理负担。

ScanFilter 支持按以下字段过滤:

  • 设备名称(模糊匹配)
  • 设备 MAC 地址
  • Service UUID(最常用)
  • Manufacturer Specific Data
  • Service Data

举个例子,我只想扫描支持心率服务的设备:

ScanFilter filter = new ScanFilter.Builder()
    .setServiceUuid(new ParcelUuid(UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")))
    .build();

List<ScanFilter> filters = new ArrayList<>();
filters.add(filter);

嗯,这里要注意:过滤器是在蓝牙芯片层面执行的,不是等数据到了应用层再过滤。这意味着,不匹配的广播包根本不会上报到你的回调里。省电效果非常明显。

避坑指南:我曾经把 Service UUID 写错了字节序,结果死活扫不到设备。后来发现 BLE 用的是小端序,而 Java UUID 默认是大端序。记得用 ParcelUuid 来包装。

11.3 扫描回调的线程管理

扫描回调默认跑在 Binder 线程池里。说白了,就是系统的一个后台线程。如果你直接在回调里更新 UI,大概率会崩溃——因为 Android 不允许在非 UI 线程操作视图。

我建议的做法是:

  1. 在回调里只做数据收集,不做 UI 操作。
  2. 用 Handler 或 LiveData 把数据抛到主线程。
  3. 如果数据量很大,考虑用线程池做批量处理。

看一段代码示例:

private final Handler mMainHandler = new Handler(Looper.getMainLooper());

private final ScanCallback mScanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        // 这里跑在 Binder 线程
        ScanResult copy = new ScanResult(result); // 深拷贝,防止数据被覆盖
        mMainHandler.post(() -> {
            // 现在安全了,可以更新 UI
            adapter.addDevice(copy);
        });
    }

    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        // 批量结果,建议用线程池处理
        mExecutor.execute(() -> {
            List<ScanResult> processed = processResults(results);
            mMainHandler.post(() -> adapter.addAll(processed));
        });
    }
};

关键点:ScanResult 对象在回调结束后可能被系统回收。如果你需要异步处理,记得做深拷贝。我吃过这个亏,数据莫名其妙就变成 null 了。

11.4 省电策略:让扫描更聪明

省电不是简单地降低扫描频率,而是在合适的时间做合适的事。我总结了三条策略:

11.4.1 动态调整扫描参数

根据设备状态切换扫描模式。比如:

  • 应用在前台时,用平衡模式。
  • 应用退到后台,切到低功耗模式。
  • 用户主动点击“搜索”,临时切到低延迟模式。

11.4.2 使用扫描周期控制

不要一直扫。扫 3 秒,停 5 秒,效果往往比一直扫更好。因为 BLE 广播本身就有间隔,你一直扫也未必能多抓到包。

// 伪代码:周期性扫描
private void startPeriodicScan() {
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            startScan();
            mHandler.postDelayed(this, SCAN_DURATION_MS + SCAN_PAUSE_MS);
        }
    }, 0);
}

private void startScan() {
    bluetoothLeScanner.startScan(filters, settings, scanCallback);
    mHandler.postDelayed(() -> bluetoothLeScanner.stopScan(scanCallback), SCAN_DURATION_MS);
}

11.4.3 利用 Android 的 JobScheduler 或 WorkManager

如果你的扫描任务不需要实时响应,可以考虑用 WorkManager 来做后台周期性扫描。系统会帮你合并唤醒时间,减少电量消耗。

个人经验:我曾经在一个智能家居项目里,把扫描从“持续扫描”改成“每 30 秒扫 5 秒”,电量消耗降低了 60% 以上。而且设备发现率几乎没有下降。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,扫描优化不是单一维度的,而是参数、过滤、线程、省电四个环节的协同配合。

BLE 扫描优化核心逻辑 BLE 扫描 扫描参数调优 ScanFilter 回调线程管理 省电策略 scanMode reportDelay Service UUID 设备名称/MAC Handler 抛到主线程 深拷贝 ScanResult 动态切换模式 周期扫描 + WorkManager 目标:低功耗 + 高发现率 + 流畅 UI

你看,这四个方面其实是环环相扣的。参数调优决定了扫描的“力度”,过滤器决定了扫描的“精度”,线程管理保证了 UI 的“流畅度”,省电策略则让这一切可持续。

做 BLE 开发,最忌讳的就是“一把梭”——把所有参数调到最大,然后指望系统帮你搞定。真正的优化,是理解每个参数背后的权衡,然后根据场景做取舍。


公众号:蓝海资料掘金营,微信deep3321