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 线程操作视图。
我建议的做法是:
- 在回调里只做数据收集,不做 UI 操作。
- 用 Handler 或 LiveData 把数据抛到主线程。
- 如果数据量很大,考虑用线程池做批量处理。
看一段代码示例:
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% 以上。而且设备发现率几乎没有下降。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以看到,扫描优化不是单一维度的,而是参数、过滤、线程、省电四个环节的协同配合。
你看,这四个方面其实是环环相扣的。参数调优决定了扫描的“力度”,过滤器决定了扫描的“精度”,线程管理保证了 UI 的“流畅度”,省电策略则让这一切可持续。
做 BLE 开发,最忌讳的就是“一把梭”——把所有参数调到最大,然后指望系统帮你搞定。真正的优化,是理解每个参数背后的权衡,然后根据场景做取舍。