蓝牙设备扫描
本文档介绍 JieLi Android BT Demo(ATTConnect 模块)中的 BLE 设备扫描能力,涵盖扫描入口 BleManager、扫描配置、超时机制、结果回调体系 IBtScanCallback 以及扫描结果数据模型 ScanDeviceInfo 的完整实现。
Purpose and Scope
本页聚焦于**蓝牙设备扫描(Discovery/Scan)**这一核心能力,内容范围包括:
- 扫描管理器
BleManager的初始化(单例模式)与扫描环境检查; startLeScan()/stopLeScan()的完整控制流,以及 Android 5.0+(BluetoothLeScanner)与旧版 API(startLeScan)的双路径兼容实现;- 扫描参数(
ScanSettings、ScanFilter)、超时自动停止机制与消息处理; - 扫描结果回调接口
IBtScanCallback与回调管理器BleEventCallbackManager的协作方式; - 扫描结果数据模型
ScanDeviceInfo的字段与 Parcelable 序列化。
以下主题属于相邻目录页的内容,本页不展开:
- 扫描到设备后的 BLE 连接流程(
connectBleDevice、GATT 回调、MTU 协商)——参见"设备连接"页面; - 连接后的数据读写(
SendBleDataThread、OnWriteDataCallback)——参见"数据通信"页面。
Overview
在 ATTConnect 中,扫描是发现周边 BLE 设备的唯一入口,也是连接流程的前置步骤。BleManager 作为全局单例统一封装了系统蓝牙 API,对外提供:
- 一键扫描:
startLeScan(timeout)自动处理权限检查、环境检查、扫描参数构建与超时调度; - 状态查询:
isBleScanning()随时可查询扫描状态; - 事件通知:通过注册
BleEventCallback,业务层可收到扫描开始/结束、设备发现、适配器开关、扫描失败等事件。
设计上,BleManager 采用单例 + 回调分发模式:所有系统级蓝牙回调(ScanCallback / LeScanCallback / BluetoothGattCallback)集中在管理器内部处理,再通过 mCallbackManager 分发到多个业务回调订阅者。这样既避免了业务层直接接触系统 API 的复杂度,也让扫描、连接、数据读写共用一个管理器,保证状态一致(例如连接前必须停止扫描)。
Architecture
flowchart TD
subgraph sg_App["应用层 (ATTConnect)"]
UI["UI / 业务代码"]
EventCallback["BleEventCallback<br/>(implements IBtScanCallback)"]
end
subgraph sg_BleManager["BleManager (单例)"]
Instance["BleManager.getInstance()"]
ScanEntry["startLeScan(timeout)"]
StopEntry["stopLeScan()"]
ScanEnv["checkScanEnv() + 权限检查"]
ScanSettings["ScanSettings 构建<br/>LOW_LATENCY / AGGRESSIVE"]
CallbackMgr["BleEventCallbackManager"]
Handler["Handler<br/>MSG_SCAN_BLE_TIMEOUT"]
Devices["mDiscoveredBleDevices<br/>设备去重缓存"]
end
subgraph sg_Android["Android 系统蓝牙栈"]
Adapter["BluetoothAdapter"]
Scanner["BluetoothLeScanner<br/>(API 21+)"]
Legacy["startLeScan 旧版回调<br/>(API < 21)"]
end
subgraph sg_Model["数据模型"]
ScanInfo["ScanDeviceInfo<br/>(Parcelable)"]
end
UI --> Instance
UI -->|"registerBleEventCallback"| EventCallback
EventCallback --> CallbackMgr
Instance --> ScanEntry
ScanEntry --> ScanEnv
ScanEnv -->|"通过"| ScanSettings
ScanSettings --> Scanner
Scanner -->|"ScanCallback"| CallbackMgr
Adapter --> Legacy
Legacy -->|"LeScanCallback"| CallbackMgr
ScanEntry -->|"启动超时"| Handler
Handler -->|"超时自动停止"| StopEntry
CallbackMgr -->|"onDiscoveryDevice"| ScanInfo
CallbackMgr -->|"onDiscoveryState / onDiscoveryFail"| EventCallback
Devices --> CallbackMgr
架构说明:
BleManager是扫描能力的唯一入口(单例),持有BluetoothAdapter与BluetoothLeScanner(API 21+)两个系统句柄,以及扫描状态标志isBleScanning与已发现设备缓存mDiscoveredBleDevices;BleEventCallbackManager负责将系统回调(扫描结果、扫描失败、状态变化)广播给所有注册的BleEventCallback,业务层实现IBtScanCallback接口即可订阅;ScanDeviceInfo是扫描结果的载体,携带BluetoothDevice、RSSI、原始广播数据(rawData)与连接状态,并通过equals/hashCode以设备为维度去重;- 扫描超时由内部
Handler的MSG_SCAN_BLE_TIMEOUT消息驱动,超时后自动调用stopLeScan(),避免扫描永久运行消耗电量。
核心实现解析
BleManager 单例与初始化
BleManager 采用**双重检查锁(Double-Checked Locking)**的单例模式,通过 getInstance() 获取唯一实例。构造函数中完成两件关键初始化:
- 获取系统
BluetoothAdapter(BluetoothAdapter.getDefaultAdapter()); - 在 Android 5.0(LOLLIPOP)及以上版本,通过适配器获取
BluetoothLeScanner——这是新扫描 API 的入口。
private BleManager(Context context) {
if (null == context) {
throw new RuntimeException("Context can not be null.");
}
mContext = context;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothAdapter != null) {
mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
}
registerReceiver();
}
public static BleManager getInstance() {
if (instance == null) {
synchronized (BleManager.class) {
if (instance == null) {
instance = new BleManager(MyApplication.Companion.getApplication());
JL_Log.w(TAG, "init", "instance : " + instance);
}
}
}
return instance;
}
Source: BleManager.java
设计意图:将 BluetoothLeScanner 的获取放在构造阶段而非每次扫描时获取,避免重复查询系统服务;同时 getInstance() 使用应用级 Context(MyApplication.Companion.getApplication()),防止 Activity 上下文泄漏。
扫描入口:startLeScan(timeout)
startLeScan(long timeout) 是扫描能力的核心方法,其控制流可以拆解为四个阶段:
阶段一:环境与权限检查
@SuppressLint("MissingPermission")
public boolean startLeScan(long timeout) {
if (!checkScanEnv("startLeScan")) return false;
if (!PermissionUtil.INSTANCE.isHasLocationPermission(mContext)) {
JL_Log.w(TAG, "startLeScan", "Missing location permissions.");
return false;
}
if (timeout <= 0) timeout = SCAN_BLE_TIMEOUT;
Source: BleManager.java
checkScanEnv()负责校验蓝牙适配器是否存在、是否持有蓝牙扫描权限(hasBluetoothScanPermission);- 位置权限是 Android 上扫描 BLE 广播的传统前提(Android 12 之前定位权限决定能否拿到广播结果),缺失时直接返回
false并记录警告日志; timeout <= 0时回退到默认超时常量SCAN_BLE_TIMEOUT。
阶段二:重复扫描处理(幂等保护)
if (isBleScanning()) {
JL_Log.i(TAG, "startLeScan", "BLE is searching.");
if (mBluetoothLeScanner != null && Build.VERSION.SDK_INT >= LOLLIPOP) {
mBluetoothLeScanner.flushPendingScanResults(mScanCallback);
}
mDiscoveredBleDevices.clear();
mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT);
mHandler.sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, timeout);
isBleScanning(true);
syncSystemBleDevice();
return true;
}
Source: BleManager.java
若扫描已在运行,不会重复调用系统 startScan(系统层会报错),而是:
- 调用
flushPendingScanResults()让系统立即回调已缓存的扫描结果; - 清空设备缓存并重置超时计时(先移除旧消息再发送新消息),相当于"续扫";
- 重新同步系统已绑定设备。这保证了重复调用是幂等的。
阶段三:双路径启动(版本兼容)
boolean ret;
if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothLeScanner != null) {
ScanSettings scanSettings;
int scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY; //修改搜索BLE模式 -- 均衡模式
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ScanSettings.Builder builder = new ScanSettings.Builder()
.setScanMode(scanMode)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED);
}
scanSettings = builder.build();
} else {
scanSettings = new ScanSettings.Builder()
.setScanMode(scanMode)
.build();
}
List<ScanFilter> filters = new ArrayList<>();
mBluetoothLeScanner.startScan(filters, scanSettings, mScanCallback);
ret = true;
} else {
ret = mBluetoothAdapter.startLeScan(mLeScanCallback);
}
Source: BleManager.java
- API 21+ 路径:使用
BluetoothLeScanner.startScan(filters, scanSettings, mScanCallback)。filters为空列表,即不过滤任何广播,扫描全部周边 BLE 设备; - 旧版路径(API < 21 或 scanner 为空):回退到
BluetoothAdapter.startLeScan(mLeScanCallback),结果通过LeScanCallback回调; - 两种路径共用一个状态机:
isBleScanning(ret)会把扫描状态广播给业务层。
阶段四:启动成功后的收尾
JL_Log.i(TAG, "startLeScan", BluetoothUtil.formatString("%s. timeout : %d.", ret, timeout));
isBleScanning(ret);
if (ret) {
mDiscoveredBleDevices.clear();
mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT);
mHandler.sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, timeout);
syncSystemBleDevice();
}
return ret;
}
Source: BleManager.java
启动成功后清空历史设备缓存(避免与上一次扫描结果混淆)、调度超时消息,并调用 syncSystemBleDevice() 将系统已绑定/已连接的设备补充进发现列表——这保证了"已配对设备即使不在广播范围内也能出现在列表里"。
停止扫描:stopLeScan()
@SuppressLint("MissingPermission")
public boolean stopLeScan() {
if (!checkScanEnv("stopLeScan")) return false;
if (!isBleScanning()) return false;
try {
if (Build.VERSION.SDK_INT >= LOLLIPOP && mBluetoothLeScanner != null) {
mBluetoothLeScanner.stopScan(mScanCallback);
} else {
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
} catch (Exception e) {
e.printStackTrace();
}
mHandler.removeMessages(MSG_SCAN_BLE_TIMEOUT);
mHandler.removeMessages(MSG_SCAN_HID_DEVICE);
isBleScanning(false);
return true;
}
Source: BleManager.java
- 与启动对称地走双路径停止;
try/catch包裹系统调用,防止系统内部异常(如蓝牙关闭瞬间)导致崩溃; - 同时移除
MSG_SCAN_BLE_TIMEOUT与MSG_SCAN_HID_DEVICE两个消息——后者表明 HID 设备发现逻辑也挂在扫描生命周期上; - 通过
isBleScanning(false)广播"扫描结束"状态。
扫描状态管理
扫描状态由布尔字段 isBleScanning 维护,其 setter 是唯一的"状态广播点":
public boolean isBleScanning() {
return isBleScanning;
}
private void isBleScanning(boolean isScanning) {
isBleScanning = isScanning;
mCallbackManager.onDiscoveryState(isScanning);
}
Source: BleManager.java 与 BleManager.java
任何扫描状态的翻转(开始/结束)都会触发 onDiscoveryState 回调,业务层可据此刷新 UI 上的扫描指示器。
回调接口 IBtScanCallback
业务层通过实现 IBtScanCallback 接口订阅扫描事件。接口定义了四类回调:
public interface IBtScanCallback {
/** 蓝牙适配器开关回调 */
void onAdapterChange(boolean bEnabled);
/** 搜索蓝牙设备的状态回调 */
void onDiscoveryState(boolean bStart);
/** 发现蓝牙设备的回调 */
void onDiscoveryDevice(ScanDeviceInfo scanDeviceInfo);
/** 搜索失败回调 */
void onDiscoveryFail(int code, String message);
}
Source: IBtScanCallback.java
| 回调 | 触发时机 | 用途 |
|---|---|---|
onAdapterChange(boolean) | 蓝牙适配器开关状态变化(由 registerReceiver() 监听的系统广播驱动) | 提示用户开启蓝牙 |
onDiscoveryState(boolean) | 扫描开始/结束(isBleScanning(boolean) 每次翻转时) | UI 扫描动画/按钮状态 |
onDiscoveryDevice(ScanDeviceInfo) | 每个新发现的 BLE 设备 | 刷新设备列表 |
onDiscoveryFail(int, String) | 扫描失败(如系统返回 SCAN_FAILED_* 错误码) | 错误提示与重试 |
扫描结果模型 ScanDeviceInfo
ScanDeviceInfo 实现了 Parcelable,可在组件间安全传递,核心字段包括:
public class ScanDeviceInfo implements Parcelable {
/** 蓝牙设备 */
private final BluetoothDevice device;
/** 信号强度 */
private int rssi;
/** 是否允许连接(默认允许连接,特殊情况不允许) */
private boolean isEnableConnect = true;
/** 原始数据 */
private byte[] rawData;
/** 连接状态 */
private int connection = BluetoothProfile.STATE_DISCONNECTED;
...
}
Source: ScanDeviceInfo.java
device是最终不可变字段,作为设备唯一标识;equals()/hashCode()仅以device为维度比较——这意味着同一个 MAC 地址的设备只会出现在列表一次,新的广播结果会更新既有条目(RSSI/rawData)而非新增条目;toString()通过CHexConver.byte2HexStr(rawData)以十六进制字符串打印广播原始数据,便于日志排查;isEnableConnect默认true,供上层业务按需标记"不可连接"设备(例如白名单外设备)。
核心流程:一次完整的扫描生命周期
sequenceDiagram
participant UI as 业务代码 (UI)
participant BM as BleManager (单例)
participant ENV as checkScanEnv / 权限检查
participant SYS as 系统蓝牙栈 (BluetoothLeScanner)
participant CB as BleEventCallbackManager
participant DEV as ScanDeviceInfo
UI->>BM: startLeScan(10000)
BM->>ENV: 检查适配器 / 扫描权限 / 定位权限
ENV-->>BM: 通过
BM->>BM: 已在扫描? 是则续扫 (flush + 重置超时)
BM->>SYS: startScan([], ScanSettings(LOW_LATENCY), mScanCallback)
SYS-->>BM: 启动成功
BM->>CB: onDiscoveryState(true)
CB-->>UI: 扫描开始 (更新UI)
BM->>BM: sendEmptyMessageDelayed(MSG_SCAN_BLE_TIMEOUT, 10000)
loop 每次收到广播
SYS->>BM: onScanResult(callbackType, result)
BM->>DEV: 构建 ScanDeviceInfo (device/rssi/rawData)
BM->>BM: 按 device 去重后加入 mDiscoveredBleDevices
BM->>CB: onDiscoveryDevice(scanDeviceInfo)
CB-->>UI: 刷新设备列表
end
Note over BM: 10 秒后 Handler 触发 MSG_SCAN_BLE_TIMEOUT
BM->>BM: stopLeScan() 自动停止
BM->>SYS: stopScan(mScanCallback)
BM->>CB: onDiscoveryState(false)
CB-->>UI: 扫描结束 (更新UI)
流程说明:
- 业务层调用
startLeScan(timeout),管理器依次完成环境检查(适配器非空、扫描权限、定位权限); - 若扫描已在进行,则走"续扫"路径:
flushPendingScanResults()立即取回系统缓存的广播,并重置超时计时器; - 首次启动时,按 API 版本选择
BluetoothLeScanner.startScan(API 21+)或startLeScan(旧版),随后广播onDiscoveryState(true)并调度超时消息; - 扫描期间,每个广播回调都被转换为
ScanDeviceInfo,按设备去重后通过onDiscoveryDevice分发; - 超时消息触发自动停止:移除超时与 HID 相关消息、调用
stopScan、广播onDiscoveryState(false)。整个生命周期无需业务层额外干预。
使用示例
示例一:注册扫描回调并启动扫描
业务层实现 IBtScanCallback(或继承 BleEventCallback)注册到 BleManager,随后调用 startLeScan 开始扫描。扫描结果在 onDiscoveryDevice 中刷新列表:
// 注册回调(BleEventCallback 实现了 IBtScanCallback,可只重写关心的回调)
mCallback = new BleEventCallback() {
@Override
public void onDiscoveryState(boolean bStart) {
// 更新扫描动画/按钮
}
@Override
public void onDiscoveryDevice(ScanDeviceInfo scanDeviceInfo) {
// 将 scanDeviceInfo 加入列表适配器并 notifyDataSetChanged()
}
@Override
public void onDiscoveryFail(int code, String message) {
// 提示用户扫描失败
}
};
BleManager.getInstance().registerBleEventCallback(mCallback);
// 开始扫描,10 秒超时
boolean ret = BleManager.getInstance().startLeScan(10000);
Source(注册/注销入口与扫描调用): BleManager.java
示例二:读取扫描结果数据
onDiscoveryDevice 收到的 ScanDeviceInfo 可直接读取设备、信号强度与原始广播数据,用于列表展示或过滤:
public ScanDeviceInfo setRssi(int rssi) {
this.rssi = rssi;
return this;
}
public boolean isEnableConnect() {
return isEnableConnect;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScanDeviceInfo that = (ScanDeviceInfo) o;
return Objects.equals(device, that.device);
}
@Override
public int hashCode() {
return Objects.hashCode(device);
}
Source: ScanDeviceInfo.java
setRssi 返回 this 支持链式调用;equals/hashCode 仅基于 device,保证同一设备在集合中的唯一性。
示例三:停止扫描与销毁清理
扫描结束或页面销毁时必须显式停止扫描,避免系统资源占用:
public void destroy() {
JL_Log.w(TAG, "destroy", "instance : " + instance);
unregisterReceiver();
stopConnectTimeout();
clearConnectedBleDevices();
if (isBleScanning()) stopLeScan();
isBleScanning(false);
mDiscoveredBleDevices.clear();
mCallbackManager.release();
mHandler.removeCallbacksAndMessages(null);
instance = null;
}
Source: BleManager.java
destroy() 是资源回收的总入口:停止扫描、清空设备缓存、释放回调、移除所有 Handler 消息并将单例置空。注意它在 isBleScanning() 为真时才调用 stopLeScan(),且之后显式将状态置为 false——这是为了防止广播风暴或重复停止。
另外,connectBleDevice 在建立连接前也会先 stopLeScan()(见 BleManager.java),因为 Android 系统不允许扫描与连接并发使用同一 GATT 回调通道,先停扫再连接是保证连接稳定性的关键步骤。
配置选项
扫描行为的配置集中在 BleManager 内部,业务层通过方法参数控制:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
SCAN_BLE_TIMEOUT | long | 常量(本页所读片段未展示具体数值) | 默认扫描超时时间(毫秒),timeout <= 0 时回退到该值 |
startLeScan(timeout) 参数 | long | SCAN_BLE_TIMEOUT | 单次扫描超时;超时后 Handler 自动触发 stopLeScan() |
ScanSettings.SCAN_MODE_LOW_LATENCY | int | 固定值 | 低延迟扫描模式,最快回报广播,但更耗电(代码注释标注为"均衡模式") |
ScanSettings.MATCH_MODE_AGGRESSIVE | int | API 23+ | 激进匹配模式,尽量回报更多广告包(厂商私有广播通常仅周期性出现一次) |
ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT | int | API 23+ | 单次匹配回报的广告包数量上限设为最大 |
ScanSettings.PHY_LE_ALL_SUPPORTED | int | API 26+ | 同时扫描 1M/2M/编码 PHY,兼容更广的设备 |
ScanFilter 列表 | List | 空列表 | 不设过滤条件,扫描全部 BLE 设备 |
设计意图:LOW_LATENCY + AGGRESSIVE + MAX_ADVERTISEMENT 的组合是面向厂商私有广播协议(如杰理芯片设备的上报广播)的典型配置——这类设备广播周期短、载荷大,只有激进的扫描策略才能稳定捕获;代价是功耗上升,因此必须配合超时自动停止机制。
API 参考
BleManager.getInstance(): BleManager
获取全局唯一实例(双重检查锁单例)。首次调用时使用应用级 Context 构造,初始化 BluetoothAdapter 与 BluetoothLeScanner。
- 返回:
BleManager实例 - 抛出:
RuntimeException(Context 为 null 时,见构造函数)
Source: BleManager.java
startLeScan(timeout: long): boolean
启动 BLE 扫描。
- 参数:
timeout— 扫描超时毫秒数;<= 0时使用默认值SCAN_BLE_TIMEOUT - 返回:
true表示扫描已启动(或已在扫描并续扫);false表示环境/权限检查失败 - 失败原因:适配器为 null、缺少蓝牙扫描权限、缺少定位权限(分别记录对应警告日志)
- 副作用:清空设备缓存、调度超时消息、同步系统已绑定设备、广播
onDiscoveryState(true)
Source: BleManager.java
stopLeScan(): boolean
停止 BLE 扫描。
- 返回:
true表示已停止;false表示环境检查失败或本就未在扫描 - 副作用:移除
MSG_SCAN_BLE_TIMEOUT与MSG_SCAN_HID_DEVICE消息、广播onDiscoveryState(false)
Source: BleManager.java
isBleScanning(): boolean
查询当前是否正在扫描。
Source: BleManager.java
registerBleEventCallback(callback: BleEventCallback) / unregisterBleEventCallback(callback: BleEventCallback)
注册/注销扫描事件订阅者。内部委托给 BleEventCallbackManager,支持多订阅者。
Source: BleManager.java
destroy()
释放全部扫描/连接资源:停止扫描、清空缓存、释放回调管理器、移除 Handler 消息、单例置空。应用退出时调用。
Source: BleManager.java
IBtScanCallback 接口方法
| 方法签名 | 参数 | 触发条件 |
|---|---|---|
onAdapterChange(boolean bEnabled) | 适配器开关状态 | 系统蓝牙开关广播 |
onDiscoveryState(boolean bStart) | 扫描是否进行中 | isBleScanning() 状态翻转 |
onDiscoveryDevice(ScanDeviceInfo scanDeviceInfo) | 扫描结果对象 | 每个新设备广播 |
onDiscoveryFail(int code, String message) | 错误码与描述 | 系统扫描失败回调 |
Source: IBtScanCallback.java
ScanDeviceInfo 关键方法
getDevice(): BluetoothDevice— 设备对象(不可变)getRssi(): int/setRssi(int): ScanDeviceInfo— 信号强度(链式)isEnableConnect(): boolean/setEnableConnect(boolean): ScanDeviceInfo— 是否允许连接getRawData(): byte[]/setRawData(byte[]): ScanDeviceInfo— 广播原始数据getConnection(): int/setConnection(int): ScanDeviceInfo— 连接状态(BluetoothProfile常量)equals/hashCode— 以device为唯一性维度- 实现
Parcelable(CREATOR工厂),支持跨组件传递
Source: ScanDeviceInfo.java
失败模式、边界情况与并发
环境与权限失败
| 场景 | 表现 | 处理 |
|---|---|---|
设备不支持蓝牙(mBluetoothAdapter == null) | checkScanEnv 记录警告 "No support for Bluetooth function." | startLeScan / stopLeScan 直接返回 false |
缺少扫描权限(hasBluetoothScanPermission) | 记录 "Missing permission to search Bluetooth." | 返回 false,由业务层引导用户授权 |
| 缺少定位权限 | startLeScan 记录 "Missing location permissions." | 返回 false;这是 Android 12 之前获取广播结果的前提 |
| 蓝牙未开启 | 扫描可被系统拒绝或返回空结果 | 通过 onAdapterChange(false) 通知 UI 引导开启 |
扫描期间调用连接
connectBleDevice 内部会先检查 isBleScanning() 并调用 stopLeScan()(见 BleManager.java)。这是有意的串行化设计:Android BLE 栈在同一时刻处理扫描与连接会产生竞争,先停扫再连接可避免 GATT 回调与扫描回调交错导致的异常。
重复调用 startLeScan(幂等续扫)
再次调用时不会重复 startScan,而是走续扫分支:flushPendingScanResults() 拉取缓存、清空列表、重置超时。这使 UI 层的"下拉刷新/重扫"操作可以安全地重复触发。
并发与状态一致性
- 所有扫描状态翻转都经由
isBleScanning(boolean)单一入口广播,避免状态与回调不一致; - 单例通过
synchronized(BleManager.class)保护创建,但扫描启动本身不持锁——Android 系统回调运行在 Binder 线程,业务层应只读ScanDeviceInfo并尽快拷贝数据到 UI 线程; - 超时消息用
removeMessages+sendEmptyMessageDelayed模式管理,续扫/停止时都会先移除旧消息,杜绝"已停止扫描仍收到超时回调"的竞态; destroy()在isBleScanning()为真时先停扫再清空缓存,并removeCallbacksAndMessages(null)清空所有待处理消息,保证单例重建时无残留状态。
系统扫描失败
API 21+ 的 ScanCallback.onScanFailed 会携带错误码(如 SCAN_FAILED_ALREADY_STARTED、SCAN_FAILED_APPLICATION_REGISTRATION_FAILED、SCAN_FAILED_FEATURE_UNSUPPORTED)。管理器将这些错误转换为 onDiscoveryFail(code, message) 通知业务层,业务层应据此提示用户并给出重试路径。
性能与运维注意事项
- 功耗:
SCAN_MODE_LOW_LATENCY回报快但耗电高;必须依赖超时自动停止机制,业务层无需(也不应)依赖用户手动停止; - 回调频率:
MATCH_MODE_AGGRESSIVE + MATCH_NUM_MAX_ADVERTISEMENT下同一设备会高频回报广告包。ScanDeviceInfo.equals以device为维度去重,mDiscoveredBleDevices是去重缓存——UI 层应复用该缓存而非每次全量刷新列表; - 线程模型:系统扫描回调在 Binder 线程触发,
ScanDeviceInfo的Parcelable支持安全传递,但列表适配器的notifyDataSetChanged()必须切回主线程; - 日志:扫描关键路径均使用
JL_Log(startLeScan/stopLeScan/ 状态翻转)记录,ScanDeviceInfo.toString()以十六进制输出rawData,线上排查广播协议问题时可直接从日志定位; - Android 12+ 权限变化:从 Android 12 开始,扫描需要
BLUETOOTH_SCAN运行时权限(hasBluetoothScanPermission已覆盖);Android 13+ 不再要求定位权限即可扫描,但本实现的定位检查仍然保留,向下兼容旧版本。
扩展点
- 自定义扫描过滤:
startLeScan内部构建了空ScanFilter列表(扫描全部设备)。如需按 Service UUID/厂商数据过滤,可在mBluetoothLeScanner.startScan(filters, ...)处传入定制ScanFilter列表——当前实现刻意保持全量扫描,把过滤职责留给业务层; - 多订阅者回调:通过
registerBleEventCallback(BleEventCallback)可注册多个订阅者,BleEventCallbackManager统一分发。业务层可选择实现完整IBtScanCallback,或继承BleEventCallback只重写关心的回调(如只关心onDiscoveryDevice); - 扫描超时策略:
timeout参数由调用方决定,UI 层可结合业务场景(首次搜索给更长超时、刷新给更短超时)动态传入; - 设备可连接性标记:
ScanDeviceInfo.isEnableConnect供业务层标记"可见但不可连"的设备(如白名单过滤结果),展示层据此禁用连接按钮。
相关链接
- 扫描到的设备如何建立连接(
connectBleDevice、GATT 回调、MTU 协商)——参见"设备连接"目录页 - 连接后的数据读写(
SendBleDataThread、OnWriteDataCallback)——参见"数据通信"目录页 - 回调分发实现:BleEventCallbackManager.java
- 扫描入口与连接管理:BleManager.java
- 扫描回调接口:IBtScanCallback.java
- 扫描结果模型:ScanDeviceInfo.java
- 蓝牙权限工具:PermissionUtil(
startLeScan中定位权限检查处)