《Android BLE ScanSettings 完全解析:从参数到实战》

一、什么是 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 开发核心原则

  1. 超时必加:非后台监控场景必须设置扫描超时(建议 10-30 秒),避免无限耗电
  2. 过滤优先 :使用ScanFilter硬件过滤(比软件过滤省电 10 倍以上),减少无效回调
  3. 动态适配:根据场景分阶段切换模式(快速发现→省电监控),平衡体验与功耗
  4. 硬件检查 :批量扫描、PHY 设置等功能需先通过isOffloadedScanBatchingSupported()等方法验证硬件支持
  5. 版本兼容:对 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 设置(远距离 / 高速模式)

十二、官方资源参考

相关推荐
江上清风山间明月3 小时前
LOCAL_STATIC_ANDROID_LIBRARIES的作用
android·静态库·static_android
三少爷的鞋4 小时前
Android 中 `runBlocking` 其实只有一种使用场景
android
应用市场6 小时前
PHP microtime()函数精度问题深度解析与解决方案
android·开发语言·php
沐怡旸7 小时前
【Android】Dalvik 对比 ART
android·面试
消失的旧时光-19438 小时前
Android NDK 完全学习指南:从入门到精通
android
消失的旧时光-19438 小时前
Kotlin 协程实践:深入理解 SupervisorJob、CoroutineScope、Dispatcher 与取消机制
android·开发语言·kotlin
2501_915921438 小时前
iOS 26 描述文件管理与开发环境配置 多工具协作的实战指南
android·macos·ios·小程序·uni-app·cocoa·iphone
2501_915909068 小时前
iOS 抓包实战 从原理到复现、定位与真机取证全流程
android·ios·小程序·https·uni-app·iphone·webview
2501_915106329 小时前
HBuilder 上架 iOS 应用全流程指南:从云打包到开心上架(Appuploader)上传的跨平台发布实践
android·ios·小程序·https·uni-app·iphone·webview