第3章:BLE连接生命周期与依赖注入容器:连接池管理、设备句柄注入时机、断开后的资源清理边界

3.1 架构边界:连接生命周期不是业务状态,而是容器控制面

在并发BLE系统中,连接的生命周期管理是依赖注入(DI)容器的第一道边界。常见错误是:将连接状态机直接映射到业务对象上,导致连接断开时业务对象持有的句柄变成空悬引用,而新的连接请求又无法复用池中资源。架构上,连接生命周期应完全由DI容器托管的连接池控制,业务层只通过抽象接口获取临时设备句柄。

生命周期阶段架构边界DI容器角色生命周期事件
连接建立驱动层到连接池层创建设备句柄,注入连接配置ConnectionEstablished
连接活跃业务层到设备句柄管理句柄范围,限制访问时长DataTransmitted / Received
连接挂起/重连连接池内部状态迁移释放旧句柄,注册新句柄预填充ConnectionSuspended / Reconnected
连接断开资源清理边界销毁句柄,回收内存,通知订阅方ConnectionLost / Disconnected
关键概念:角色边界 — 连接生命周期的所有者是DI容器,不是业务对象。业务对象只从容器请求一个“令牌”,令牌过期由容器决定。

3.2 分层职责:连接池管理策略

连接池位于驱动适配层与业务服务层之间,负责抽象物理连接、控制并发上限、管理设备句柄的注入时机。典型连接池包含三个核心组件:

  • 连接管理器(ConnectionManager):持有真实蓝牙GATT连接,维护连接ID、RSSI、MTU等物理参数。
  • 设备句柄工厂(DeviceHandleFactory):根据连接状态创建/销毁设备句柄,句柄中封装了读写特征值的操作。
  • 资源清理器(ResourceCleaner):监听连接断开事件,异步回收回调、缓冲区、订阅通知。
// 连接池配置示例(JSON格式)
{
  "pool": {
    "max_concurrent": 4,
    "idle_timeout_ms": 30000,
    "handle_ttl_ms": 10000,
    "cleanup_grace_period_ms": 5000
  },
  "injection_policy": "on_acquire",  // on_create | on_acquire | on_connect
  "cleanup_strategy": "deferred_async"
}
踩坑提醒: 句柄注入时机若选在“连接建立时(on_create)”而非“业务请求时(on_acquire)”,会导致断开后容器内残留过期句柄,空悬引用难以追踪。推荐使用 on_acquire 策略,每次业务调用都向连接池请求有效句柄。

3.3 组件间依赖关系与注入时序

BLE服务注册与设备句柄注入必须遵循严格的序列:先通过DI容器注册服务的生命周期回调,再在连接建立后由连接池决定何时注入设备句柄。

// IDeviceHandle 接口定义
public interface IDeviceHandle : IAsyncDisposable
{
    ValueTask<ReadResult> ReadAsync(ushort handle, CancellationToken ct);
    ValueTask<WriteResult> WriteAsync(ushort handle, byte[] data, CancellationToken ct);
    event EventHandler<DisconnectionEventArgs>? Disconnected;
}

// 注册与注销序列
container.Register<IDeviceHandle>(scope: ServiceScope.Transient,
    factory: sp => pool.AcquireAsync(ct));
// 注意:不要注册为 Singleton,否则句柄将跨连接复用,造成数据交叉

3.4 控制流与数据流设计:断开后的资源清理边界

连接断开时,清理边界必须包含以下步骤:

  1. 连接管理器检测到断开,立刻将连接状态标记为待清理,阻止新句柄分配。
  2. 资源清理器接收断开事件,遍历所有活跃设备句柄,调用 Disconnected 事件通知业务方。
  3. 业务方在事件处理中取消正在进行的读写操作(捕获 OperationCanceledException)。
  4. 容器释放设备句柄的 DI 作用域,回收瞬态对象(如数据缓冲区、回调委托)。
  5. 连接池从池中移除该连接,若达到重连策略阈值,则触发重连流程。
// 资源释放流程(伪代码)
async ValueTask CleanupAsync(string deviceAddress)
{
    await _connectionManager.MarkForCleanupAsync(deviceAddress);
    var handles = _handleRegistry.GetByAddress(deviceAddress);
    foreach (var handle in handles)
    {
        handle.Disconnected?.Invoke(this, args);
        await handle.DisposeAsync();  // 触发 DI 作用域释放
    }
    _handleRegistry.RemoveByAddress(deviceAddress);
    _connectionPool.Remove(deviceAddress);
}

3.5 选型权衡:连接池模式对比

模式适用场景优点风险点
单连接直连低并发、单设备场景实现简单,无池开销断连后完全阻塞,无复用能力
固定大小连接池多设备、固定连接上限资源可控,句柄复用快连接数超过池大小时请求排队,可能饥饿
动态伸缩连接池高并发、设备动态进出弹性好,利用率高清理不及时会累积空悬句柄,增加GC压力
架构推荐: 初始使用固定连接池(大小 = 设备数 * 1.2),结合 on_acquire 注入和 deferred_async 清理,是平衡复杂度与可靠性的最优妥协。

3.6 风险点与妥协策略

  • 空悬设备句柄:断开后业务方仍持有引用。妥协策略:在 IDeviceHandle 中嵌入版本号或连接ID,每次操作前校验有效性。
  • 并发清理竞态:多个连接同时断开导致清理冲突。妥协策略:为每个连接引入清理锁,使用异步队列串行化清理。
  • 注入时机与事务性:句柄注入发生在业务事务中间。妥协策略:采用“两阶段注入”:先预留连接ID,事务提交前再获取真实句柄。
  • 内存泄漏:回调订阅未取消。妥协策略:使用弱事件模式(WeakEvent)或强制业务方在 Disconnected 事件中取消订阅。

3.7 落地步骤与验收标准

3.7.1 落地步骤

  1. 定义 IDeviceHandle 接口及 Disconnected 事件。
  2. 实现 ConnectionPool 核心类,包含 AcquireAsync / ReleaseAsync 方法。
  3. 在 DI 容器中注册 IDeviceHandle 为 Transient,工厂委托池的 AcquireAsync。
  4. 实现 ResourceCleaner 组件,订阅底层蓝牙驱动的断连事件。
  5. 集成到现有业务服务,替换直连方式。
  6. 编写集成测试:模拟连接建立、正常断开、异常断开、并发请求等场景。

3.7.2 验收标准

验收项通过条件检测方式
空悬引用检测断开后1秒内,所有活跃设备句柄被销毁日志监控 + 内存快照
连接池并发上限并发请求数不超过池大小,无死锁压力测试:4并发连接循环起停
句柄注入时间从请求到句柄可用不超过200ms性能基准测试
资源清理完整性清理完成后无残留订阅、缓冲区内存泄漏检测工具

3.8 内联SVG:BLE连接池状态机与DI边界

BLE连接池状态机与DI容器边界 连接池内部(连接管理器) 待分配(Pooled) 连接中(Connecting) 活跃(Active) 挂起(Suspended) 断开(Disconnected) 清理后回池 依赖注入容器边界 设备句柄工厂 句柄缓存(Handle Map) 资源清理器 事件总线(Disconnected) 业务服务(消费方) 句柄请求/释放 并发边界 信号量(Semaphore) 最大连接数控制 清理锁(Cleanup Lock) 防止竞态清理 句柄版本号 检测空悬引用
图释: 左侧连接池管理物理连接的5种状态;中间DI容器边界负责句柄工厂、缓存、清理和事件广播;右侧并发控制组件确保边界安全。箭头表示控制流转移方向,虚线表示策略辅助。