第2章:依赖注入核心原则在BLE中的映射
并发BLE通信场景中存在大量职责交叉的组件,如连接管理器、服务发现器、广播控制器与GATT回调处理器。依赖注入(DI)的四条核心原则——控制反转、接口隔离、作用域管理、生命周期绑定——为这些组件提供了清晰的边界和可测试的协作模式。本章从架构设计角度逐一映射到BLE领域,重点关注边界识别、分层职责及选型风险。
2.1 控制反转 —— 谁掌握连接生命周期?
在非DI的BLE实现中,高层业务(如数据同步Service)直接通过“new BLEConnection()”创建连接,同时负责重连、断开及错误处理。控制反转要求将连接对象的创建与生命周期管理反转给一个独立的容器或工厂组件。
| 角色 | 非DI做法 | DI反转后 |
|---|---|---|
| 连接管理器 | 业务层直接调用Android BluetoothDevice.connectGatt() | 业务层注入ConnectionManager接口,实际实现由容器提供 |
| GATT回调 | 匿名内部类散落在各Activity/Fragment | 回调对象由DI容器管理,通过生命周期钩子自动清理 |
| 重连策略 | 硬编码在应用主线程中 | 作为独立策略组件注入,可热替换 |
控制反转的边界定义在“谁拥有
BluetoothGatt 对象引用”。DI容器持有该引用,业务组件只获得经过封装的 ConnectionHandle。任何直接调用 gatt.connect() 的代码都越界。
// 反向后的典型接口定义
interface ConnectionManager {
fun connect(deviceAddress: String): ConnectionHandle
fun disconnect(handle: ConnectionHandle)
fun onConnected(handle: ConnectionHandle, gatt: BluetoothGatt)
}
// 业务层只操作 ConnectionHandle,无需接触原生对象
class DataSyncService(private val connMgr: ConnectionManager) {
fun syncDevice(address: String) {
val handle = connMgr.connect(address)
handle.discoverServices()
}
}
2.2 接口隔离 —— 按BLE职责切分接口
一个大型“BLEManager”接口会同时暴露扫描、连接、服务发现、特征读写等方法,导致依赖它的任何组件都受所有方法变更影响。接口隔离原则要求每个组件只依赖其所需的最小接口集。常见的BLE接口切片如下:
Scanner:仅暴露startScan()和stopScan()Connector:仅包含connect(deviceAddress): ConnectionHandle与disconnect(handle)ServiceDiscoverer:提供discoverServices(handle): List<BLEService>CharacteristicIO:处理read(characteristic)、write(characteristic, data)及通知注册ConnectionStateObserver:仅暴露回调式状态变更事件
BluetoothGatt 对象,接口隔离必须在合理的抽象层级上进行——建议控制在5~7个接口以内,超过8个通常意味着设计过度。
2.3 作用域管理 —— BLE连接范围的依赖共享
一个BLE连接对象(BluetoothGatt)以及其上的服务、特征缓存、MTU状态等,应当在同一连接作用域内共享。作用域管理决定了某个依赖实例的生存范围与可见性。
| 作用域 | 共享对象示例 | 生命周期 |
|---|---|---|
| 无作用域(单例) | BluetoothAdapter、线程池 | Application生命期 |
| 连接作用域 | BluetoothGatt、特征缓存、断开回调 | 从connect()到disconnect() |
| 服务作用域 | BLE服务对象、特征值快照 | 服务发现完成后到连接断开 |
| 传输作用域 | 一次read/write操作上下文 | 单个请求-响应内 |
实现方式通常依赖DI容器(如Kodein、Dagger Hilt)的作用域注解。在BLE场景中,连接作用域必须与Android BLE连接对象的实际回调绑定,避免作用域泄漏。
// 以Kodein为例定义作用域
val connectionScope = Scope(singleton = false) {
bind<BluetoothGatt>() with scoped(scope = this) {
// 回调中创建,断开时销毁
create { ... provideGatt() }
destroy { it.close() }
}
bind<CharacteristicCache>() with scoped(scope = this) {
create { CharacteristicCache() }
}
}
2.4 生命周期绑定 —— 依赖存活与BLE事件同步
BLE的异步事件(如连接建立、服务发现完成、断开回调)与依赖创建/销毁的时序必须严格对齐。生命周期绑定原则要求:依赖的创建和销毁由这些BLE事件驱动,而非简单的Activity或Fragment生命周期。
- 连接建立事件:当
onConnectionStateChange返回 STATE_CONNECTED 时,DI容器应当立即创建连接作用域并注入BluetoothGatt对象。 - 服务发现完成:触发服务作用域的构建,注入
BLEService和CharacteristicIO接口的实现。 - 断开事件:在所有操作完成(包括写队列清空)后,DI容器依次销毁作用域内的所有单次依赖,释放资源。
- 异常断开(如超出范围):同样触发销毁,且重连逻辑应当重新走完整的作用域创建流程,而非复用旧实例。
使用观察者模式或协程Flow监听BLE状态变化,通过DI容器的
scope.release()或container.unbind()方法显式回收作用域。注意在断开回调中不要持有活动线程,防止内存泄漏。
2.5 四原则的协同边界图
下图展示了依赖注入四条核心原则在BLE通信中的映射关系和组件边界。控制反转将连接对象管理权委托给DI容器;接口隔离按照职责切割出5个独立接口;作用域管理在连接与服务粒度上划定共享范围;生命周期绑定则将依赖的存活与BLE事件挂钩。
2.6 选型权衡与妥协策略
- 编译期DI(Dagger Hilt) vs 运行时DI(Kodein/koin):编译期DI在BLE场景中更安全(错误可提前发现),但作用域配置较复杂;运行时DI便于热替换和BLE反注册。妥协方案:核心连接作用域使用编译期DI,其他扩展接口用运行时DI。
- 作用域粒度:过粗的作用域(整个App共享一个连接对象)不安全;过细(每次读写都创建新作用域)增加开销。建议在连接断开前维持同一作用域,内部通过子作用域管理服务发现和缓存。
- 生命周期回调与DI容器解耦:避免在BLE回调(如onConnectionStateChange)中直接调用容器操作方法。引入一个中间“LifecycleAdapter”组件,将回调转换为容器指令。
2.7 落地步骤与验收标准
| 步骤 | 内容 |
|---|---|
| 1. 接口定义 | 按2.2节切片定义5~7个BLE专用接口,确保每个接口方法数不超过5个 |
| 2. 作用域实现 | 创建ConnectionScope、ServiceScope注解/对象,并在DI容器中注册对应生命周期 |
| 3. 事件绑定 | 编写BLEDiBinder组件,监听BLE状态变化,触发作用域的创建/销毁 |
| 4. 单元测试 | 注入mock接口验证作用域内依赖的唯一性及生命周期事件的正确触发 |
| 5. 集成测试 | 模拟BLE连接/断开循环,检查无作用域泄漏、无僵尸连接 |
✓ 每个BLE连接生命周期内,
BluetoothGatt 实例唯一且仅被DI容器持有✓ 断开连接后无法再通过旧
ConnectionHandle 执行操作✓ 接口隔离原则被满足:没有组件依赖超过其需要的3个接口
✓ 并发连接场景下(如同时连接多个设备),各连接作用域互不干扰