4. 扫描服务实现:封装系统蓝牙扫描API,回调转发

4.1 原理概述

系统蓝牙扫描API(如 Android 的 BluetoothLeScanner)通常以单次回调或重复扫描的方式暴露结果,但在复杂模块化架构中,扫描结果需要向多个订阅者、不同生命周期组件或跨进程分发。直接在每个调用处注册系统回调会导致代码分散、资源管理混乱。因此需要将系统扫描API封装成一个独立的扫描服务,统一管理扫描状态,并通过观察者模式、事件总线或协程通道将回调转发给业务模块。

设计目标:对外暴露简洁的启动/停止接口,对内将系统原始回调转换为可复用的结果流,实现扫描逻辑与业务逻辑的解耦。

4.2 实现流程

  1. 定义扫描结果回调接口:抽象出统一的 ScanResultCallback,包含 onScanResult(ScanResult)、onScanFailed(ScanFailureReason) 等。
  2. 封装扫描管理器:实现 ScannerManager 类,持有系统 BluetoothLeScanner 的引用,管理扫描生命周期(start/stop)。
  3. 注册回调转发:管理一个回调列表(或协程通道),当系统触发 onScanResult 时,依次通知所有已注册的订阅者。
  4. 线程模型处理:系统回调通常在主线程或特定 Binder 线程,转发时根据订阅者需求切换到指定线程(如 UI 线程、后台线程)。
  5. 错误与异常分级:捕获系统回调中的异常,通过失败回调或日志上报。

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:回调泄漏 — 未在组件销毁时取消注册,导致无法回收内存或回调已销毁的组件。解决方案:在 onDestroy / onStop 中显式调用 unregisterCallback。
错误2:重复启动扫描 — 连续调用 startScan 未检查状态,造成系统异常。解决方案:添加 isScanning 锁或状态机。
错误3:线程阻塞 — 在系统回调线程中执行耗时操作。解决方案:始终切换线程后在回调中处理。

4.8 性能与稳定性优化

  • 回调去重:避免相同结果重复分发,使用已扫描设备 MAC 地址缓存(内存 LRU 缓存,5分钟过期)。
  • 回调池+批处理:当大量结果瞬间涌入(如扫描过滤较宽),使用 ExecutorService 批处理分发减少锁竞争。
  • 扫描超时自动停止:内置定时器,超过设定时间后自动调用 stopScan,同时触发 onScanStopped 回调通知业务层。
  • 稳定扫描间隔:对于后台扫描场景,使用 WorkManager 配合扫描服务,避免频繁唤醒系统。

4.9 扫描封装架构图

扫描服务封装与回调转发流程 扫描服务封装与回调转发架构 业务订阅者 (N个) 扫描服务接口 扫描管理器 系统蓝牙扫描API 回调转发队列 线程切换 · 去重 · 超时

4.10 本章小结