一、什么是 ScanSettings
ScanSettings 是 Android BLE 扫描的策略控制核心,通过 Builder 模式构建。它的核心作用是定义 "如何扫描"------ 在扫描速度、设备功耗、结果准确性之间找到最优平衡,是高效 BLE 开发的关键配置类。
开发者通过它可灵活控制:
- 扫描优先级:追求快速响应还是低功耗?
- 结果上报:实时返回还是批量延迟?
- 匹配精度:远距离弱信号也检测,还是只关注近距离强信号?
高频场景模板(直接复用)
🧩 高速实时扫描(配对页 / 设备搜索页)
scss
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 最高频率扫描
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) // 弱信号也匹配
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT) // 多包确认,降低误报
.setReportDelay(0) // 实时上报结果
.build()
🧩 平衡型扫描(常规设备列表页)
scss
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED) // 速度与功耗平衡
.setMatchMode(ScanSettings.MATCH_MODE_STICKY) // 仅匹配强信号
.setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT) // 2-4包确认
.build()
🧩 低功耗扫描(后台监控 / 长期检测)
scss
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 最低功耗
.setMatchMode(ScanSettings.MATCH_MODE_STICKY) // 强信号过滤
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) // 1包触发
.setReportDelay(10_000) // 10秒批量上报,减少回调
.build()
二、核心参数总览(快速查阅)
| 配置方法 | 核心功能 | 常见取值 | 默认值 | 关键说明 |
|---|---|---|---|---|
| setScanMode() | 扫描频率 / 功耗策略 | LOW_POWER/BALANCED/LOW_LATENCY/OPPORTUNISTIC | BALANCED | 决定扫描速度与功耗的核心平衡点 |
| setCallbackType() | 回调触发时机 | ALL_MATCHES/FIRST_MATCH/MATCH_LOST | ALL_MATCHES | 控制是否实时上报、仅上报首次发现 / 设备丢失 |
| setMatchMode() | 广播匹配灵敏度 | AGGRESSIVE/STICKY | STICKY | 定义触发回调的信号强度阈值 |
| setNumOfMatches() | 单设备上报次数限制 | ONE/FEW/MAX | FEW | 控制重复结果,仅配合 FIRST_MATCH 生效 |
| setReportDelay() | 批量回调延迟(ms) | 0(实时)/ 自定义延迟(如 10000) | 0 | 延迟越大功耗越低,需硬件支持 |
| setLegacy() | 广播格式兼容 | true(仅传统)/false(传统 + 扩展) | true | Android 8.0+ 支持,影响 BLE 5.0 特性使用 |
三、核心配置方法详解(附实战注意事项)
3.1 setScanMode () - 扫描模式(最影响功耗)
功能:控制扫描频率和功耗分配,是性能优化的核心参数。
| 模式常量 | 扫描频率 | 典型参数 | 功耗等级 | 适用场景 |
|---|---|---|---|---|
| SCAN_MODE_LOW_POWER | 最低 | 512ms 扫描 / 4096ms 间隔 | ⭐ 极低 | 后台长期监控、省电需求场景 |
| SCAN_MODE_BALANCED | 中等 | 1024ms 扫描 / 4096ms 间隔 | ⭐⭐ 中等 | 日常列表页、常规使用(默认) |
| SCAN_MODE_LOW_LATENCY | 最高 | 几乎持续扫描 | ⭐⭐⭐ 高 | 快速配对、前台实时搜索 |
| SCAN_MODE_OPPORTUNISTIC | 被动 | 依赖其他应用扫描结果 | 几乎为零 | 搭便车模式(不可单独使用) |
实战要点:
- LOW_LATENCY 功耗极高,建议限制使用时长(如 30 秒后切换为低功耗模式)。
- OPPORTUNISTIC 模式需配合其他应用的扫描,单独使用无效果。
- 模式切换会立即生效,无需重启扫描。
scss
// 示例:前台快速扫描30秒后切换省电模式
builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
handler.postDelayed(() -> {
stopScan();
builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);
startScan();
}, 30000); // 30秒后切换
3.2 setCallbackType () - 回调触发方式
功能:定义 ScanCallback 的触发时机,直接影响回调频率和实时性。
| 回调类型 | 触发时机 | 回调频率 | 适用场景 |
|---|---|---|---|
| CALLBACK_TYPE_ALL_MATCHES | 每次收到匹配广播 | 极高 | 实时监控、RSSI 信号分析(默认) |
| CALLBACK_TYPE_FIRST_MATCH | 设备首次进入扫描范围 | 低 | 设备发现、区域进入检测 |
| CALLBACK_TYPE_MATCH_LOST | 设备长时间未被扫描到 | 低 | 设备离开检测、超时提醒 |
组合使用技巧:
- 位运算组合多种类型,实现 "进入 + 离开" 监控(如门禁系统)。
- 非实时场景优先使用 FIRST_MATCH/MATCH_LOST,减少回调消耗。
| 组合方式 | 效果 | 典型应用 |
|---------------|--------------|-------------|-----------|
| `FIRST_MATCH | MATCH_LOST` | 监控设备进入/离开 | 门禁系统、区域感知 |
| ALL_MATCHES | 持续获取所有广播 | 信号强度监控、数据采集 |
swift
// 示例:监控设备进入/离开范围
builder.setCallbackType(
ScanSettings.CALLBACK_TYPE_FIRST_MATCH |
ScanSettings.CALLBACK_TYPE_MATCH_LOST
);
// 回调处理
@Override
public void onScanResult(int callbackType, ScanResult result) {
if (callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH) {
Log.d("BLE", "设备进入: " + result.getDevice().getName());
} else if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
Log.d("BLE", "设备离开: " + result.getDevice().getName());
}
}
3.3 setReportDelay () - 批量报告延迟
功能:设置扫描结果的批量上报延迟,启用批处理以降低功耗。
| 参数值 | 行为特点 | 回调方法 | 功耗影响 | 适用场景 |
|---|---|---|---|---|
| 0(默认) | 实时上报每一条结果 | onScanResult() | 高 | 实时响应需求(如配对页) |
| >0(毫秒) | 延迟后批量返回结果 | onBatchScanResults() | 低 | 后台扫描、数据采集 |
关键注意事项:
- 需先通过
bluetoothAdapter.isOffloadedScanBatchingSupported()检查硬件支持。 - 批量模式下结果缓存在硬件,延迟时间越长,单次回调数据量越大。
java
// 示例:安全启用批量扫描
ScanSettings.Builder builder = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);
if (bluetoothAdapter.isOffloadedScanBatchingSupported()) {
builder.setReportDelay(10000); // 10秒批量上报
Log.d("BLE", "批量扫描已启用");
} else {
Log.d("BLE", "设备不支持批量扫描,使用实时模式");
}
// 批量回调处理
@Override
public void onBatchScanResults(List<ScanResult> results) {
Log.d("BLE", "批量接收 " + results.size() + " 条结果");
for (ScanResult result : results) {
// 处理设备数据
}
}
3.4 setMatchMode () - 匹配模式(API 23+)
功能:控制触发回调的信号强度阈值,平衡检测距离与稳定性。
| 匹配模式 | 信号要求 | 响应速度 | 误报率 | 适用场景 |
|---|---|---|---|---|
| MATCH_MODE_AGGRESSIVE | 弱信号可匹配(约 - 90dBm) | 快 | 高 | 远距离检测、快速发现设备 |
| MATCH_MODE_STICKY | 强信号才匹配(约 - 70dBm) | 慢 | 低 | 近距离可靠连接、减少误触发 |
对比总结:
- AGGRESSIVE 适合 "广撒网"(如初次搜索设备)。
- STICKY 适合 "精准定位"(如连接前确认设备在近距离)。
scss
// 场景1:快速发现所有可能设备(含远距离弱信号)
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
// 场景2:仅关注近距离强信号设备(连接更稳定)
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
3.5 setNumOfMatches () - 匹配数量(API 23+)
功能:设置触发 FIRST_MATCH 回调前,需接收的广播包数量(防误报)。
| 数量常量 | 所需广播包 | 触发速度 | 可靠性 | 功耗 | 适用场景 |
|---|---|---|---|---|---|
| MATCH_NUM_ONE_ADVERTISEMENT | 1 个 | 最快 | 低 | 最低 | 极速响应(如快速配对) |
| MATCH_NUM_FEW_ADVERTISEMENT | 2-4 个 | 中等 | 中等 | 中等 | 常规设备发现(默认) |
| MATCH_NUM_MAX_ADVERTISEMENT | 尽可能多 | 最慢 | 最高 | 最高 | 工业级应用、防误触发 |
使用规则:
- 仅与 CALLBACK_TYPE_FIRST_MATCH 配合生效,与 ALL_MATCHES 连用无效。
- 数量越多,误报率越低,但触发延迟越高。
典型组合:
| 组合 | 效果 | 适用场景 |
|---|---|---|
FIRST_MATCH + ONE_ADVERTISEMENT |
极速响应 | 快速配对流程 |
FIRST_MATCH + FEW_ADVERTISEMENT |
平衡可靠 | 常规设备发现 |
FIRST_MATCH + MAX_ADVERTISEMENT |
高度可靠 | 工业级应用、防误触发 |
scss
// 示例:快速发现且降低误报(平衡方案)
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT) // 2-4包确认
.build();
3.6 setLegacy () - 广播类型(API 26+)
功能:控制扫描的广播格式,决定是否支持 BLE 5.0 扩展广播。
| 参数值 | 扫描范围 | 兼容性 | 最大数据包 | 适用场景 |
|---|---|---|---|---|
| true | 仅 BLE 4.x 传统广播 | 全兼容 | ≤31 字节 | 适配旧设备(默认) |
| false | 传统广播 + BLE 5.0 扩展广播 | BLE 5.0+ | ≤255 字节 | 需传输大量数据、用新特性 |
BLE 广播类型核心差异:
- 传统广播(BLE 4.x):兼容性强,但数据量小、传输距离有限。
- 扩展广播(BLE 5.0):数据量提升 8 倍,支持远距离 / 高速传输,仅新设备支持。
arduino
// 示例:根据设备需求选择广播类型
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean needBle5 = true; // 需支持BLE 5.0特性时设为true
builder.setLegacy(!needBle5); // false = 启用扩展广播
}
3.7 setPhy () - 物理层类型(API 26+)
功能:指定扫描使用的 PHY 层,影响传输速率、距离和抗干扰能力(BLE 5.0+ 特性)。
| PHY 类型 | 传输速率 | 最大距离 | 功耗 | 适用场景 |
|---|---|---|---|---|
| PHY_LE_ALL_SUPPORTED | 自适应 | 自适应 | 平衡 | 通用场景(默认推荐) |
| PHY_LE_1M | 1Mbps | ~50m | 中等 | 标准应用、兼容性优先 |
| PHY_LE_CODED | 125/500Kbps | ~200m | 低 | 远距离设备(如智能家居) |
PHY 核心特性对比:
| 特性 | 1M PHY(默认) | Coded PHY(远距离) | 2M PHY(连接用) |
|---|---|---|---|
| 蓝牙版本要求 | BLE 4.0+ | BLE 5.0+ | BLE 5.0+ |
| 抗干扰能力 | 标准 | 强(FEC 编码) | 弱 |
| 典型用途 | 常规设备通信 | 远距离传感器、定位 | 高速数据传输 |
scss
// 示例:远距离扫描(如户外传感器)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setLegacy(false) // 启用BLE 5.0
.setPhy(BluetoothDevice.PHY_LE_CODED) // 远距离PHY
.build();
}
四、兼容性速查表(避免踩坑)
| 配置方法 | 最低 API 级别 | 对应 Android 版本 | 关键注意事项 |
|---|---|---|---|
| setScanMode() | 21 | 5.0(Lollipop) | 全模式基础支持,无额外限制 |
| setCallbackType() | 21 | 5.0+ | FIRST_MATCH/MATCH_LOST 需 API 23+ |
| setReportDelay() | 21 | 5.0+ | 需通过硬件支持检查,否则无效 |
| setMatchMode() | 23 | 6.0(Marshmallow) | 仅 API 23+ 可用,低版本需兼容处理 |
| setNumOfMatches() | 23 | 6.0+ | 仅配合 FIRST_MATCH 生效 |
| setLegacy() | 26 | 8.0(Oreo) | BLE 5.0 特性,低版本不支持 |
| setPhy() | 26 | 8.0+ | 需设备支持 BLE 5.0,否则默认使用 1M PHY |
五、实战配置方案
5.1 场景对照表(快速选型)
| 应用场景 | 扫描模式 | 回调类型 | 批量延迟 | 匹配模式 | 预期功耗 |
|---|---|---|---|---|---|
| 🔴 前台快速配对 | LOW_LATENCY | ALL_MATCHES | 0 | AGGRESSIVE | 高 |
| 🟡 日常设备搜索 | BALANCED | ALL_MATCHES | 0 | STICKY | 中 |
| 🟢 后台持续监控 | LOW_POWER | FIRST_MATCH | 5000+ | STICKY | 低 |
| 🔵 门禁进出检测 | BALANCED | FIRST/LOST | 0 | STICKY | 中低 |
| 🟣 信号强度采集 | LOW_LATENCY | ALL_MATCHES | 0 | AGGRESSIVE | 高 |
5.2 可直接复用的配置代码
方案 1:前台快速配对(功耗敏感度低)
scss
/**
* 场景:用户点击"搜索设备",需立即显示结果
* 特点:最快响应,短时间使用,功耗可接受
*/
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 持续高频扫描
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 实时返回结果
.setReportDelay(0) // 无延迟上报
.build();
// ⚠️ 必加:30秒后自动停止,避免过度耗电
handler.postDelayed(() -> stopScan(), 30000);
方案 2:后台长时间监控(极致省电)
scss
/**
* 场景:智能手环监控、室内定位
* 特点:最低功耗,容忍延迟,长时间运行
*/
ScanSettings.Builder builder = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER); // 基础省电模式
// Android 6.0+ 额外优化
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 仅监控设备首次出现和消失
builder.setCallbackType(
ScanSettings.CALLBACK_TYPE_FIRST_MATCH |
ScanSettings.CALLBACK_TYPE_MATCH_LOST
);
builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY); // 过滤弱信号
builder.setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT); // 减少误报
// 硬件支持则启用批量扫描
if (bluetoothAdapter.isOffloadedScanBatchingSupported()) {
builder.setReportDelay(10000); // 10秒批量上报
}
}
ScanSettings settings = builder.build();
方案 3:门禁系统(进出检测)
scss
/**
* 场景:检测蓝牙卡进出办公区域
* 特点:可靠检测,中等功耗
*/
ScanSettings.Builder builder = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED); // 平衡速度与功耗
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 监听"进入"和"离开"事件
builder.setCallbackType(
ScanSettings.CALLBACK_TYPE_FIRST_MATCH | // 设备进入
ScanSettings.CALLBACK_TYPE_MATCH_LOST // 设备离开
);
builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY); // 避免弱信号误触发
builder.setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT); // 多包确认
}
ScanSettings settings = builder.build();
// 配合过滤器,只扫描门禁蓝牙卡
ScanFilter filter = new ScanFilter.Builder()
.setDeviceName("AccessCard") // 目标设备名称
.build();
scanner.startScan(Arrays.asList(filter), settings, new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
if (callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH) {
Log.d("Access", "员工进入: " + result.getDevice().getAddress());
// 执行开门逻辑
} else if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
Log.d("Access", "员工离开: " + result.getDevice().getAddress());
}
}
});
方案 4:BLE 5.0 远距离传感器网络
scss
/**
* 场景:仓库温湿度传感器(BLE 5.0 Coded PHY)
* 特点:覆盖范围大,数据包大,依赖 BLE 5.0
*/
ScanSettings.Builder builder = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER); // 低功耗基础
// Android 8.0+ 启用 BLE 5.0 特性
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setLegacy(false) // 启用扩展广播(支持255字节数据包)
.setPhy(BluetoothDevice.PHY_LE_CODED); // 远距离物理层
}
// Android 6.0+ 优化回调
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
// 批量处理传感器数据
if (bluetoothAdapter.isOffloadedScanBatchingSupported()) {
builder.setReportDelay(60000); // 每分钟汇总一次
}
}
ScanSettings settings = builder.build();
方案 5:RSSI 信号强度实时监控
java
/**
* 场景:室内定位、距离估算、信号分析
* 特点:高频获取 RSSI 数据,短时间密集使用
*/
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 最高频率扫描
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // 实时上报
.setReportDelay(0) // 无延迟
.build();
// 启动扫描并计算设备距离
scanner.startScan(null, settings, new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
int rssi = result.getRssi(); // 信号强度
int txPower = result.getTxPower(); // 发射功率
double distance = calculateDistance(rssi, txPower); // 距离估算
Log.d("Distance", "设备距离: " + String.format("%.2f", distance) + " 米");
}
// RSSI 转距离公式(参考实现)
private double calculateDistance(int rssi, int txPower) {
if (rssi == 0) return -1.0; // 无效信号
double ratio = rssi * 1.0 / txPower;
return ratio < 1.0 ? Math.pow(ratio, 10) : 0.89976 * Math.pow(ratio, 7.7095) + 0.111;
}
});
六、权限配置(避坑核心)
6.1 完整 Manifest 配置
xml
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 位置权限(所有版本扫描蓝牙必需) -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 传统蓝牙权限(Android 11 及以下) -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- 新蓝牙权限(Android 12+) -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
tools:targetApi="s" />
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 声明设备支持 BLE -->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<application>
...
</application>
</manifest>
6.2 分版本权限对照表
| Android 版本 | 必需权限 | 关键说明 |
|---|---|---|
| 5.0 - 5.1(API 21-22) | BLUETOOTH + BLUETOOTH_ADMIN | 无需运行时请求,清单声明即可 |
| 6.0 - 11(API 23-30) | BLUETOOTH + BLUETOOTH_ADMIN +(ACCESS_COARSE_LOCATION 或 ACCESS_FINE_LOCATION) | 需运行时请求位置权限 |
| 12+(API 31+) | BLUETOOTH_SCAN + BLUETOOTH_CONNECT + ACCESS_COARSE_LOCATION + ACCESS_FINE_LOCATION | 新蓝牙权限 + 位置权限缺一不可 |
⚠️ 常见误区:Android 12+ 仅需 BLUETOOTH_SCAN 无需位置权限?正确结论:需同时获取新蓝牙权限和位置权限(除非明确不使用设备 MAC 地址)。
6.3 位置权限的核心逻辑
| 场景 | 是否需要位置权限 | 原因 |
|---|---|---|
| 扫描并获取 MAC 地址 | ✅ 是 | MAC 地址可用于定位,系统强制要求 |
| 扫描但不使用 MAC 地址 | ❌ 否 | 需给 BLUETOOTH_SCAN 加 neverForLocation 标记 |
| 连接已知设备 | ❌ 否 | 仅需 BLUETOOTH_CONNECT 权限 |
xml
<!-- 不使用 MAC 地址的配置(功能受限) -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<!-- 此时 device.getAddress() 会返回随机地址 -->
推荐做法 :除非确定不需要 MAC 地址,否则始终请求位置权限。
6.4 运行时权限请求工具类(Kotlin)
kotlin
class BlePermissionHelper {
companion object {
private const val REQUEST_CODE_BLE = 110
}
/**
* 请求蓝牙扫描所需全部权限
*/
fun requestBlePermissions(activity: Activity) {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+:4个权限
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
} else {
// Android 6-11:仅位置权限
arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
}
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE_BLE)
}
/**
* 检查是否已授予所有必需权限
*/
fun hasAllBlePermissions(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
hasPermission(context, Manifest.permission.BLUETOOTH_SCAN) &&
hasPermission(context, Manifest.permission.BLUETOOTH_CONNECT) &&
hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) &&
hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
} else {
hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ||
hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
}
}
private fun hasPermission(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
6.5 扫描前完整检查流程
java
public class BleScanStarter {
private static final int REQUEST_ENABLE_BT = 111;
/**
* 启动扫描前的5步检查
*/
public boolean startScanWithCheck(Activity activity) {
// 1. 检查设备是否支持 BLE
if (!activity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Toast.makeText(activity, "设备不支持蓝牙 BLE", Toast.LENGTH_SHORT).show();
return false;
}
// 2. 检查蓝牙是否开启
BluetoothManager bluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
if (bluetoothAdapter == null) {
Toast.makeText(activity, "设备不支持蓝牙", Toast.LENGTH_SHORT).show();
return false;
}
if (!bluetoothAdapter.isEnabled()) {
// 跳转开启蓝牙
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
return false;
}
// 3. Android 6-11 检查位置服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
LocationManager locationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
boolean isLocationEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|| locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
if (!isLocationEnabled) {
Toast.makeText(activity, "请开启位置服务", Toast.LENGTH_LONG).show();
// 跳转位置设置
activity.startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
return false;
}
}
// 4. 检查权限
if (!new BlePermissionHelper().hasAllBlePermissions(activity)) {
new BlePermissionHelper().requestBlePermissions(activity);
return false;
}
// 5. 所有检查通过,启动扫描
startBleScan(bluetoothAdapter);
return true;
}
private void startBleScan(BluetoothAdapter adapter) {
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
ScanSettings settings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build();
scanner.startScan(null, settings, scanCallback);
Log.d("BLE", "扫描已启动");
}
}
6.6 权限请求结果处理
less
@Override
public void onRequestPermissionsResult(
int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 110) {
boolean allGranted = true;
StringBuilder deniedMsg = new StringBuilder("以下权限被拒绝:\n");
// 遍历权限结果
for (int i = 0; i < permissions.length; i++) {
boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
if (!granted) {
allGranted = false;
String permissionName = permissions[i].substring(permissions[i].lastIndexOf('.') + 1);
deniedMsg.append("- ").append(permissionName).append("\n");
}
}
if (allGranted) {
Toast.makeText(this, "权限已授予,开始扫描", Toast.LENGTH_SHORT).show();
startBleScan(); // 启动扫描
} else {
deniedMsg.append("\n蓝牙扫描将无法正常工作");
// 弹窗提示重新授权
new AlertDialog.Builder(this)
.setTitle("权限不足")
.setMessage(deniedMsg.toString())
.setPositiveButton("重新授权", (dialog, which) -> new BlePermissionHelper().requestBlePermissions(this))
.setNegativeButton("取消", null)
.show();
}
}
}
6.7 权限关键要点总结
- Android 12+ 必需:BLUETOOTH_SCAN + BLUETOOTH_CONNECT + 位置权限
- Android 6-11 必需:至少一个位置权限(ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION)
- 位置权限用途:获取设备真实 MAC 地址(系统强制要求)
- 位置服务要求:Android 6-11 需开启,12+ 不强制
- 清单配置:用 maxSdkVersion 限制旧权限仅作用于 Android
6.8 权限快速检查清单(调试用)
typescript
public class BlePermissionChecklist {
public static void printChecklist(Context context) {
Log.d("BLE", "========== 蓝牙扫描权限检查清单 ==========");
// 1. 硬件支持
boolean hasBle = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
Log.d("BLE", "[" + (hasBle ? "✓" : "✗") + "] 设备支持 BLE");
// 2. 蓝牙状态
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
boolean hasAdapter = adapter != null;
boolean isBtEnabled = hasAdapter && adapter.isEnabled();
Log.d("BLE", "[" + (hasAdapter ? "✓" : "✗") + "] 蓝牙适配器可用");
Log.d("BLE", "[" + (isBtEnabled ? "✓" : "✗") + "] 蓝牙已开启");
// 3. 分版本权限检查
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+
Log.d("BLE", "[" + (hasPermission(context, Manifest.permission.BLUETOOTH_SCAN) ? "✓" : "✗") + "] BLUETOOTH_SCAN");
Log.d("BLE", "[" + (hasPermission(context, Manifest.permission.BLUETOOTH_CONNECT) ? "✓" : "✗") + "] BLUETOOTH_CONNECT");
Log.d("BLE", "[" + (hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ? "✓" : "✗") + "] ACCESS_COARSE_LOCATION");
Log.d("BLE", "[" + (hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ? "✓" : "✗") + "] ACCESS_FINE_LOCATION");
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android 6-11
Log.d("BLE", "[" + (hasPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ? "✓" : "✗") + "] ACCESS_COARSE_LOCATION");
Log.d("BLE", "[" + (hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ? "✓" : "✗") + "] ACCESS_FINE_LOCATION");
// 位置服务检查
LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
boolean isLocationEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
|| locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
Log.d("BLE", "[" + (isLocationEnabled ? "✓" : "✗") + "] 位置服务已开启");
}
Log.d("BLE", "===========================================");
}
private static boolean hasPermission(Context context, String permission) {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
}
七、扫描失败错误码(问题速解)
错误码对照表
| 错误码 | 常量名 | 含义 | 解决方案 |
|---|---|---|---|
| 1 | SCAN_FAILED_ALREADY_STARTED | 扫描已在运行 | 先调用 stopScan () 停止旧扫描,1 秒后重启 |
| 2 | SCAN_FAILED_APPLICATION_REGISTRATION_FAILED | 应用注册失败 | 重启蓝牙适配器(关闭再开启蓝牙) |
| 3 | SCAN_FAILED_INTERNAL_ERROR | 蓝牙内部错误 | 检查设备蓝牙状态,必要时重启设备 |
| 4 | SCAN_FAILED_FEATURE_UNSUPPORTED | 功能不支持 | 降级配置(使用 BALANCED 模式 + 基础回调) |
| 5 | SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES | 硬件资源耗尽 | 关闭其他扫描应用,5 秒后重试 |
| 6 | SCAN_FAILED_SCANNING_TOO_FREQUENTLY | 扫描过于频繁 | 增加扫描间隔(至少 30 秒),避免连续启停 |
错误处理示例代码
ini
@Override
public void onScanFailed(int errorCode) {
String errorMsg;
switch (errorCode) {
case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
errorMsg = "扫描已在运行,正在重启";
stopScan();
handler.postDelayed(this::startScan, 1000);
break;
case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
errorMsg = "应用注册失败,尝试重启蓝牙";
restartBluetooth(); // 自定义重启蓝牙方法
break;
case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
errorMsg = "蓝牙内部错误,请检查设备";
break;
case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
errorMsg = "设备不支持该扫描功能,已降级配置";
useFallbackSettings(); // 启用降级配置
break;
case ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES:
errorMsg = "蓝牙资源耗尽,5秒后重试";
handler.postDelayed(this::startScan, 5000);
break;
case ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY:
errorMsg = "扫描过于频繁,30秒后重试";
handler.postDelayed(this::startScan, 30000);
break;
default:
errorMsg = "未知扫描错误: " + errorCode;
}
Log.e("BLE_SCAN", "扫描失败: " + errorMsg);
Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show();
}
/**
* 降级配置:使用最基础兼容的扫描设置
*/
private void useFallbackSettings() {
ScanSettings fallbackSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build();
scanner.startScan(null, fallbackSettings, scanCallback);
}
八、性能优化技巧(省电 + 高效)
8.1 动态切换扫描模式(分阶段策略)
java
/**
* 阶段1:快速扫描30秒(快速发现设备)
* 阶段2:切换省电模式(持续监控)
*/
public class AdaptiveScanStrategy {
private static final long FAST_SCAN_DURATION = 30_000; // 30秒
private BluetoothLeScanner scanner;
private List<ScanFilter> filters;
private ScanCallback scanCallback;
public void startAdaptiveScan() {
// 阶段1:快速扫描
ScanSettings fastSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.build();
scanner.startScan(filters, fastSettings, scanCallback);
Log.d("BLE", "阶段1:快速扫描模式");
// 30秒后切换阶段2
handler.postDelayed(() -> {
scanner.stopScan(scanCallback);
// 阶段2:省电模式
ScanSettings powerSaveSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build();
scanner.startScan(filters, powerSaveSettings, scanCallback);
Log.d("BLE", "阶段2:省电监控模式");
}, FAST_SCAN_DURATION);
}
}
8.2 限制扫描时长(避免无限扫描)
csharp
/**
* 智能扫描管理器:自动超时停止
*/
public class SmartScanManager {
private Runnable timeoutRunnable;
private BluetoothLeScanner scanner;
private ScanCallback scanCallback;
/**
* 启动扫描并设置超时
* @param durationMs 扫描时长(毫秒)
*/
public void startTimedScan(long durationMs) {
startScan(); // 启动扫描逻辑
// 设置超时停止
timeoutRunnable = () -> {
stopScan();
Log.d("BLE", "扫描超时自动停止(时长:" + durationMs + "ms)");
};
handler.postDelayed(timeoutRunnable, durationMs);
}
public void stopScan() {
scanner.stopScan(scanCallback);
if (timeoutRunnable != null) {
handler.removeCallbacks(timeoutRunnable); // 取消超时任务
}
}
}
8.3 批量结果优化(减少回调消耗)
scss
/**
* 批量扫描结果处理:去重+批量更新UI
*/
@Override
public void onBatchScanResults(List<ScanResult> results) {
// 去重:同一设备保留最新结果(按MAC地址分组)
Map<String, ScanResult> deviceMap = new HashMap<>();
for (ScanResult result : results) {
String mac = result.getDevice().getAddress();
deviceMap.put(mac, result); // 新结果覆盖旧结果
}
// 批量处理+更新UI(减少UI刷新次数)
List<ScanResult> uniqueResults = new ArrayList<>(deviceMap.values());
processDeviceBatch(uniqueResults); // 批量业务处理
updateUIBatch(uniqueResults); // 批量UI更新
Log.d("BLE", "批量处理 " + uniqueResults.size() + " 个唯一设备");
}
8.4 电池状态自适应优化
csharp
/**
* 根据电池电量动态调整扫描策略
*/
public class BatteryAwareScan {
private Context context;
public ScanSettings getBatteryOptimizedSettings() {
// 获取电池状态
int batteryLevel = getBatteryLevel();
boolean isLowPowerMode = isLowPowerMode();
ScanSettings.Builder builder = new ScanSettings.Builder();
if (isLowPowerMode || batteryLevel < 20) {
// 低电量/省电模式:极致省电
builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
}
Log.d("BLE", "电池优化:极致省电(电量:" + batteryLevel + "%)");
} else if (batteryLevel < 50) {
// 中等电量:平衡模式
builder.setScanMode(ScanSettings.SCAN_MODE_BALANCED);
Log.d("BLE", "电池优化:平衡模式(电量:" + batteryLevel + "%)");
} else {
// 高电量:性能优先
builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
Log.d("BLE", "电池优化:性能模式(电量:" + batteryLevel + "%)");
}
return builder.build();
}
/**
* 获取电池电量百分比
*/
private int getBatteryLevel() {
Intent batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
return (int) ((level / (float) scale) * 100);
}
/**
* 检查是否开启省电模式
*/
private boolean isLowPowerMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return powerManager.isPowerSaveMode();
}
return false;
}
}
九、调试与日志工具(问题定位)
9.1 详细扫描日志工具类
typescript
public class BleScanLogger {
private static final String TAG = "BLE_SCAN";
private long scanStartTime;
private int deviceCount = 0;
private Map<String, Integer> deviceRssiMap = new HashMap<>();
/**
* 记录扫描开始
*/
public void logScanStart(ScanSettings settings) {
scanStartTime = System.currentTimeMillis();
deviceCount = 0;
deviceRssiMap.clear();
StringBuilder sb = new StringBuilder();
sb.append("\n========== 扫描开始 ==========\n");
sb.append("时间: ").append(new Date()).append("\n");
sb.append("扫描模式: ").append(getScanModeName(settings.getScanMode())).append("\n");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
sb.append("回调类型: ").append(getCallbackTypeName(settings.getCallbackType())).append("\n");
sb.append("匹配模式: ").append(getMatchModeName(settings.getMatchMode())).append("\n");
sb.append("匹配数量: ").append(getNumMatchesName(settings.getNumOfMatches())).append("\n");
}
sb.append("延迟报告: ").append(settings.getReportDelayMillis()).append(" ms\n");
sb.append("==============================\n");
Log.d(TAG, sb.toString());
}
/**
* 记录发现设备
*/
public void logDeviceFound(ScanResult result) {
deviceCount++;
BluetoothDevice device = result.getDevice();
int rssi = result.getRssi();
deviceRssiMap.put(device.getAddress(), rssi);
Log.d(TAG, String.format(
"发现设备 #%d: %s [%s] RSSI=%d dBm",
deviceCount,
device.getName() != null ? device.getName() : "Unknown",
device.getAddress(),
rssi
));
}
/**
* 记录扫描结束
*/
public void logScanStop() {
long duration = System.currentTimeMillis() - scanStartTime;
StringBuilder sb = new StringBuilder();
sb.append("\n========== 扫描结束 ==========\n");
sb.append("持续时间: ").append(duration).append(" ms\n");
sb.append("发现设备: ").append(deviceCount).append(" 个\n");
if (!deviceRssiMap.isEmpty()) {
sb.append("\n设备列表:\n");
for (Map.Entry<String, Integer> entry : deviceRssiMap.entrySet()) {
sb.append(" - ").append(entry.getKey())
.append(" (").append(entry.getValue()).append(" dBm)\n");
}
}
sb.append("==============================\n");
Log.d(TAG, sb.toString());
}
// 辅助方法:获取配置名称
private String getScanModeName(int mode) {
switch (mode) {
case ScanSettings.SCAN_MODE_LOW_POWER: return "LOW_POWER";
case ScanSettings.SCAN_MODE_BALANCED: return "BALANCED";
case ScanSettings.SCAN_MODE_LOW_LATENCY: return "LOW_LATENCY";
case ScanSettings.SCAN_MODE_OPPORTUNISTIC: return "OPPORTUNISTIC";
default: return "UNKNOWN(" + mode + ")";
}
}
private String getCallbackTypeName(int type) {
List<String> types = new ArrayList<>();
if ((type & ScanSettings.CALLBACK_TYPE_ALL_MATCHES) != 0)
types.add("ALL_MATCHES");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if ((type & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0)
types.add("FIRST_MATCH");
if ((type & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0)
types.add("MATCH_LOST");
}
return types.isEmpty() ? "NONE" : String.join(" | ", types);
}
private String getMatchModeName(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
switch (mode) {
case ScanSettings.MATCH_MODE_AGGRESSIVE: return "AGGRESSIVE";
case ScanSettings.MATCH_MODE_STICKY: return "STICKY";
}
}
return "UNKNOWN";
}
private String getNumMatchesName(int num) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
switch (num) {
case ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT: return "ONE";
case ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT: return "FEW";
case ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT: return "MAX";
}
}
return "UNKNOWN";
}
}
9.2 ScanSettings 默认值验证工具(调试用)
如果你想在代码中验证当前的默认值,可以使用反射或实际测试:
typescript
/**
* 验证 ScanSettings 的默认值(调试用)
*/
public class ScanSettingsDefaultValidator {
/**
* 打印默认配置的所有参数
*/
public static void printDefaultValues() {
ScanSettings defaultSettings = new ScanSettings.Builder().build();
StringBuilder sb = new StringBuilder();
sb.append("\n========== ScanSettings 默认值 ==========\n");
// 扫描模式
sb.append("扫描模式: ");
sb.append(getScanModeName(defaultSettings.getScanMode()));
sb.append(" (").append(defaultSettings.getScanMode()).append(")\n");
// 回调类型
sb.append("回调类型: ");
sb.append(getCallbackTypeName(defaultSettings.getCallbackType()));
sb.append(" (").append(defaultSettings.getCallbackType()).append(")\n");
// 延迟报告
sb.append("延迟报告: ");
sb.append(defaultSettings.getReportDelayMillis()).append(" ms\n");
// Android 6.0+ 参数
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
sb.append("匹配模式: ");
sb.append(getMatchModeName(defaultSettings.getMatchMode()));
sb.append(" (").append(defaultSettings.getMatchMode()).append(")\n");
sb.append("匹配数量: ");
sb.append(getNumMatchesName(defaultSettings.getNumOfMatches()));
sb.append(" (").append(defaultSettings.getNumOfMatches()).append(")\n");
}
// Android 8.0+ 参数
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
sb.append("广播类型: ");
sb.append(defaultSettings.getLegacy() ? "Legacy only" : "Legacy + Extended");
sb.append(" (").append(defaultSettings.getLegacy()).append(")\n");
sb.append("物理层: ");
sb.append(getPhyName(defaultSettings.getPhy()));
sb.append(" (").append(defaultSettings.getPhy()).append(")\n");
}
sb.append("==========================================\n");
Log.d("BLE_DEFAULT", sb.toString());
}
// 辅助方法(前面教程中已定义的 getScanModeName 等方法)
private static String getScanModeName(int mode) {
switch (mode) {
case ScanSettings.SCAN_MODE_LOW_POWER: return "LOW_POWER";
case ScanSettings.SCAN_MODE_BALANCED: return "BALANCED ✓"; // 标记默认值
case ScanSettings.SCAN_MODE_LOW_LATENCY: return "LOW_LATENCY";
case ScanSettings.SCAN_MODE_OPPORTUNISTIC: return "OPPORTUNISTIC";
default: return "UNKNOWN";
}
}
private static String getCallbackTypeName(int type) {
if (type == ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
return "ALL_MATCHES ✓"; // 标记默认值
}
List<String> types = new ArrayList<>();
if ((type & ScanSettings.CALLBACK_TYPE_ALL_MATCHES) != 0)
types.add("ALL_MATCHES");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if ((type & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0)
types.add("FIRST_MATCH");
if ((type & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0)
types.add("MATCH_LOST");
}
return types.isEmpty() ? "NONE" : String.join(" | ", types);
}
private static String getMatchModeName(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
switch (mode) {
case ScanSettings.MATCH_MODE_AGGRESSIVE: return "AGGRESSIVE";
case ScanSettings.MATCH_MODE_STICKY: return "STICKY ✓"; // 默认值
}
}
return "N/A";
}
private static String getNumMatchesName(int num) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
switch (num) {
case ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT: return "ONE";
case ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT: return "FEW ✓"; // 默认值
case ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT: return "MAX";
}
}
return "N/A";
}
private static String getPhyName(int phy) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (phy == ScanSettings.PHY_LE_ALL_SUPPORTED) {
return "ALL_SUPPORTED ✓"; // 默认值
} else if (phy == BluetoothDevice.PHY_LE_1M) {
return "1M";
} else if (phy == BluetoothDevice.PHY_LE_CODED) {
return "CODED";
}
}
return "N/A";
}
}
十、总结与最佳实践
10.1 场景 - 配置快速映射表
| 业务需求 | 核心配置方案 | 关键参数组合 |
|---|---|---|
| 用户主动搜索设备 | 快速扫描模式 | LOW_LATENCY + ALL_MATCHES + 30 秒超时 |
| 智能手环后台连接 | 极致省电模式 | LOW_POWER + FIRST_MATCH + 批量扫描(10 秒) |
| 门禁 / 区域进出检测 | 事件触发模式 | BALANCED + FIRST_MATCH 或 MATCH_LOST + STICKY |
| 室内定位 / RSSI 分析 | 高频信号采集模式 | LOW_LATENCY + ALL_MATCHES + AGGRESSIVE |
| 后台长期监控(低功耗) | 被动 / 低频模式 | OPPORTUNISTIC(搭便车)或 LOW_POWER + 长延迟 |
10.2 开发核心原则
- 超时必加:非后台监控场景必须设置扫描超时(建议 10-30 秒),避免无限耗电
- 过滤优先 :使用
ScanFilter硬件过滤(比软件过滤省电 10 倍以上),减少无效回调 - 动态适配:根据场景分阶段切换模式(快速发现→省电监控),平衡体验与功耗
- 硬件检查 :批量扫描、PHY 设置等功能需先通过
isOffloadedScanBatchingSupported()等方法验证硬件支持 - 版本兼容:对 6.0+、8.0 + 新增 API 做版本判断,低版本自动降级为基础配置
10.3 常见问题优化方案
| 问题现象 | 优化方向 | 配置调整示例 |
|---|---|---|
| 扫描设备慢 / 漏检 | 提高扫描频率 + 降低信号阈值 | 切换为 LOW_LATENCY + MATCH_MODE_AGGRESSIVE |
| 回调太频繁导致卡顿 / 耗电 | 提高信号阈值 + 限制匹配数量 | 切换为 STICKY + MATCH_NUM_FEW_ADVERTISEMENT |
| 需实时监控信号强度变化 | 提高灵敏度 + 允许重复上报 | AGGRESSIVE + MATCH_NUM_MAX_ADVERTISEMENT |
| 后台扫描经常失败 | 检查权限 + 提高扫描优先级 | 确保位置权限开启,切换为 BALANCED 模式 |
| 电池消耗过快 | 缩短扫描时长 + 降低扫描频率 | 限制单次扫描 10 秒,切换为 LOW_POWER 模式 |
10.4 智能扫描策略实现(进阶)
固定配置难以应对复杂场景,建议实现自适应扫描模式:
分阶段策略设计
| 阶段 | 模式配置 | 目标 |
|---|---|---|
| 主动扫描阶段 | LOW_LATENCY + AGGRESSIVE + MAX | 快速发现目标设备,提高响应速度 |
| 连接后阶段 | LOW_POWER + STICKY + ONE | 降低扫描频率,减少连接干扰 |
| 后台监控阶段 | BALANCED + STICKY + FEW + 批量延迟 | 周期性检查设备状态,平衡稳定性与功耗 |
实现代码示例(Kotlin)
scss
enum class ScanPhase { ACTIVE_SCAN, CONNECTED, BACKGROUND }
@SuppressLint("MissingPermission")
fun updateScanMode(phase: ScanPhase) {
val scanner = bluetoothAdapter.bluetoothLeScanner
val settings = when (phase) {
ScanPhase.ACTIVE_SCAN -> ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
.build()
ScanPhase.CONNECTED -> ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
.build()
ScanPhase.BACKGROUND -> ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
.build()
}
scanner.stopScan(scanCallback)
scanner.startScan(null, settings, scanCallback)
}
在不同状态下切换调用:
scss
updateScanMode(ScanPhase.ACTIVE_SCAN) // 搜索页面
updateScanMode(ScanPhase.CONNECTED) // 已连接
updateScanMode(ScanPhase.BACKGROUND) // 后台保持
升级优化方向
- 自动时长控制:ACTIVE_SCAN 阶段超过 10 秒无结果自动降级
- 信号感知调整:当设备 RSSI 稳定在 - 60dBm 以内时,降低扫描频率
- 厂商适配:针对 MIUI/ColorOS 等系统做白名单处理,避免后台扫描被系统限制
- 多线程隔离:扫描与连接操作分线程执行,避免互相阻塞
十一、版本支持说明
| 功能范围 | 最低 Android 版本 | 对应 API 级别 | 关键限制 |
|---|---|---|---|
| 基础扫描功能 | 5.0 | 21 | 无 MATCH_MODE/NumOfMatches 等高级配置 |
| 完整扫描策略 | 6.0 | 23 | 支持 MATCH_MODE / 回调类型组合等核心功能 |
| BLE 5.0 特性支持 | 8.0 | 26 | 支持扩展广播、PHY 设置(远距离 / 高速模式) |