《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 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui