4. 扫描服务实现:封装系统蓝牙扫描API,回调转发
4.1 原理概述
系统蓝牙扫描API(如 Android 的 BluetoothLeScanner)通常以单次回调或重复扫描的方式暴露结果,但在复杂模块化架构中,扫描结果需要向多个订阅者、不同生命周期组件或跨进程分发。直接在每个调用处注册系统回调会导致代码分散、资源管理混乱。因此需要将系统扫描API封装成一个独立的扫描服务,统一管理扫描状态,并通过观察者模式、事件总线或协程通道将回调转发给业务模块。
设计目标:对外暴露简洁的启动/停止接口,对内将系统原始回调转换为可复用的结果流,实现扫描逻辑与业务逻辑的解耦。
4.2 实现流程
- 定义扫描结果回调接口:抽象出统一的
ScanResultCallback,包含onScanResult(ScanResult)、onScanFailed(ScanFailureReason)等。 - 封装扫描管理器:实现
ScannerManager类,持有系统BluetoothLeScanner的引用,管理扫描生命周期(start/stop)。 - 注册回调转发:管理一个回调列表(或协程通道),当系统触发
onScanResult时,依次通知所有已注册的订阅者。 - 线程模型处理:系统回调通常在主线程或特定 Binder 线程,转发时根据订阅者需求切换到指定线程(如 UI 线程、后台线程)。
- 错误与异常分级:捕获系统回调中的异常,通过失败回调或日志上报。
4.3 核心代码实现
4.3.1 回调接口定义
public interface ScanResultCallback {
void onScanResult(ScanResult result);
void onScanFailed(@ScanFailureReason int errorCode);
void onScanStopped(); // 可选,当扫描被内部停止时通知
}
4.3.2 扫描管理器封装(关键部分)
public class ScannerManager {
private final BluetoothLeScanner scanner;
private final List<ScanResultCallback> callbacks = new CopyOnWriteArrayList<>();
private final Handler callbackHandler; // 用于线程切换
private final ScanCallback systemCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
// 分发到所有订阅者,切换到指定线程
for (ScanResultCallback cb : callbacks) {
callbackHandler.post(() -> cb.onScanResult(result));
}
}
@Override
public void onScanFailed(int errorCode) {
for (ScanResultCallback cb : callbacks) {
callbackHandler.post(() -> cb.onScanFailed(errorCode));
}
}
};
public void registerCallback(ScanResultCallback callback) {
callbacks.add(callback);
if (scanning) {
// 如果已经在扫描,新注册的回调应立即收到现有的最新结果?或等待下一次回调
}
}
public void unregisterCallback(ScanResultCallback callback) {
callbacks.remove(callback);
if (callbacks.isEmpty() && scanning) {
stopScan(); // 自动停止扫描(根据设计决定是否触发)
}
}
public void startScan(ScanSettings settings, List<ScanFilter> filters) {
if (scanning) return;
scanner.startScan(filters, settings, systemCallback);
scanning = true;
}
// 其他方法:stopScan, getState 等
}
4.4 回调转发方式对比
| 转发方式 | 线程模型 | 解耦程度 | 推荐场景 |
|---|---|---|---|
| 直接回调(同步) | 系统回调线程 | 低(调用者必须处理线程) | 简单同步测试 |
| Handler + 回调列表 | 可切换至指定 Looper | 中(需管理 Handler 生命周期) | 标准 Android 应用 |
| 协程 Channel / Flow | 自动线程切换(Dispatchers) | 高(天然背压、取消支持) | 现代 Kotlin 项目 |
| 事件总线(如 LiveData/EventBus) | 主线程默认 | 高(需关注生命周期) | 组件间轻量通信 |
4.5 调试与验证
- 日志埋点:在
onScanResult转发前打印回调数量、结果 MAC 地址,确认分发覆盖所有注册者。 - 单元测试:Mock 系统
BluetoothLeScanner,验证回调列表增减、成功/失败场景的分发次数。 - 线程检查:在回调中加入
Thread.currentThread().getName()日志,确保切换正确。 - 资源泄漏检测:使用 LeakCanary 或手动检测
ScannerManager是否在 Fragment/Activity 销毁后仍持有回调引用。
4.6 部署契约
在服务模块中注入扫描服务时,应遵循以下依赖注入规则(以 Dagger Hilt 为例):
@Module
@InstallIn(SingletonComponent::class)
object ScannerModule {
@Provides
@Singleton
fun provideScannerManager(
context: Context
): ScannerManager {
val bluetoothAdapter = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothAdapter
return ScannerManager(bluetoothAdapter.bluetoothLeScanner)
}
}
业务层通过 @Inject ScannerManager 获取实例,并调用 registerCallback 订阅扫描结果。
4.7 常见错误与规避
错误1:回调泄漏 — 未在组件销毁时取消注册,导致无法回收内存或回调已销毁的组件。解决方案:在
错误2:重复启动扫描 — 连续调用
错误3:线程阻塞 — 在系统回调线程中执行耗时操作。解决方案:始终切换线程后在回调中处理。
onDestroy / onStop 中显式调用 unregisterCallback。错误2:重复启动扫描 — 连续调用
startScan 未检查状态,造成系统异常。解决方案:添加 isScanning 锁或状态机。错误3:线程阻塞 — 在系统回调线程中执行耗时操作。解决方案:始终切换线程后在回调中处理。
4.8 性能与稳定性优化
- 回调去重:避免相同结果重复分发,使用已扫描设备 MAC 地址缓存(内存 LRU 缓存,5分钟过期)。
- 回调池+批处理:当大量结果瞬间涌入(如扫描过滤较宽),使用
ExecutorService批处理分发减少锁竞争。 - 扫描超时自动停止:内置定时器,超过设定时间后自动调用
stopScan,同时触发onScanStopped回调通知业务层。 - 稳定扫描间隔:对于后台扫描场景,使用
WorkManager配合扫描服务,避免频繁唤醒系统。