1.1 蓝牙服务架构概览
传统蓝牙 BLE(Bluetooth Low Energy)应用的实现,通常将设备扫描、连接管理、服务发现与数据交互三大核心逻辑全部堆砌在一个页面或一个 Service 中。这种“大而全”的单块架构在快速原型验证时可行,但在工程化落地时,会暴露以下痛点:
- 耦合度高:扫描逻辑与连接握手代码交织,修改扫描策略可能影响连接的建立流程。
- 难以测试:无法单独 Mock 扫描器或连接器来执行单元测试。
- 复用困难:不同业务场景(如扫描仪、信标、数据采集设备)需要重复实现相同的连接管理代码。
- 生命周期混乱:Android/iOS 等平台下,扫描回调、连接回调与 Activity/Fragment 生命周期难以有序管理。
本章将首先解构 BLE 通信的全链路,划分出清晰的模块边界,并阐述引入依赖注入(DI)来解耦模块的设计动机。
1.1.1 整体模块划分
将 BLE 通信链路按职责拆分为四个独立模块,每个模块只关注一个领域:
| 层次 | 模块名称 | 核心职责 | 对外接口粒度 |
|---|---|---|---|
| 应用层 | BleClient | 聚合扫描、连接、服务操作,暴露给 UI 层最简 API | startScan / connect / discoverServices / writeCharacteristic |
| 核心层 | ScannerModule | 蓝牙扫描管理、过滤策略、扫描回调分发 | startScan(ScanFilter) / stopScan / onScanResult |
| 核心层 | ConnectorModule | 连接建立、连接状态机、自动重连、连接参数协商 | connect(device) / disconnect / onConnectionStateChange |
| 核心层 | ServiceExplorer | 服务/特征发现、特征读写订阅、MTU 协商 | discoverServices / readCharacteristic / setNotify |
| 平台适配层 | BleAdapter | 封装平台蓝牙 API(Android BltAdapter、iOS CBCentralManager)差异 | startLeScan / stopLeScan / createBond 等系统调用 |
模块间只通过接口(Interface)交互,不依赖具体实现类。这是后续注入与替换的基础。
1.1.2 分离动机
动机一:关注点分离
ScannerModule 只关心如何找到设备,不关心连接后的数据业务;ConnectorModule 只管理连接状态,不关心扫描过滤策略。逻辑解耦后,每个模块可以独立演进。
动机二:可测试性
可以分别为 ScannerModule 和 ConnectorModule 编写 Mock 测试。单元测试只需注入 Mock 的 BleAdapter,不依赖真实蓝牙硬件。
动机三:可扩展性
当需要支持不同的连接策略(如优先连接、延迟连接、批量连接)时,只需替换 ConnectorModule 的实现,不影响扫描和服务访问代码。
1.1.3 依赖注入设计
依赖注入(DI)的核心思想是:一个对象不应自己创建它依赖的对象,而是由外部容器在创建时将依赖传递给它。对于 BLE 模块,DI 解决了以下具体问题:
- BleAdapter 的共享:整个应用只应有一个 BluetoothAdapter 实例(Android)或 CBCentralManager(iOS),DI 容器保证单例。
- 模块组装:BleClient 依赖 ScannerModule、ConnectorModule、ServiceExplorer,由容器负责将这些实例注入给 BleClient。
- 生命周期管理:DI 容器可以绑定模块生命周期到应用或页面,避免内存泄漏。
下面是一个典型的注入关系示意图:
1.1.4 注入示例:接口定义与构造注入
在 Android/Kotlin 项目中,模块分离与注入通常通过接口和构造器参数实现。下面展示 ScannerModule 的最小接口定义及注入方式:
// IScanner.kt — 扫描模块接口
interface IScanner {
fun startScan(filter: ScanFilter, callback: ScanCallback)
fun stopScan()
}
// ScannerImpl.kt — 实现类
class ScannerImpl(
private val adapter: BleAdapter // 由 DI 容器注入
) : IScanner {
override fun startScan(filter: ScanFilter, callback: ScanCallback) {
adapter.startLeScan(filter.uuid, callback)
}
override fun stopScan() {
adapter.stopLeScan()
}
}
// BleClient.kt — 聚合入口,接收注入的模块
class BleClient(
private val scanner: IScanner,
private val connector: IConnector,
private val serviceExplorer: IServiceExplorer
) {
fun startSearching() {
scanner.startScan(ScanFilter(), object : ScanCallback {
override fun onDeviceFound(device: BleDevice) {
connector.connect(device)
}
})
}
}
实践提示:使用 Dagger / Hilt (Android) 或 Koin 时,可以将 BleAdapter 标记为 @Singleton,ScannerImpl 和 ConnectorImpl 通过 @Inject constructor 自动获取依赖。iOS 端则可通过 Swinject 或手动注入实现同样的分层。
1.1.5 常见错误与调试方向
- 循环依赖:ScannerModule 引用了 ConnectorModule,而 ConnectorModule 又回引了 ScannerModule。解决:模块应设计为单向依赖,聚合器(BleClient)为唯一组合点。
- 重复创建 Adapter:每次注入都 new 一个 BluetoothAdapter,导致扫描/连接状态不一致。必须确保 DI 容器对平台适配层采用单例作用域。
- 接口过度设计:刚开始就抽象出 IConnect、IScan、IService 等 10+ 接口,增加维护成本。优先按“扫描、连接、服务”三大块抽取接口即可。
性能基线:依赖注入容器本身会带来微秒级的启动开销,但在 BLE 这种毫秒级响应的场景下,注入开销可忽略不计。主要性能瓶颈仍在蓝牙硬件的扫描间隔和连接参数上,模块分离不会引入额外的效率损失。