CoreBluetooth 数据接收与预处理:CBCentralManager / CBPeripheral 回调、Data 转 NSData、字节流边界处理

2.1 原理说明

iOS 蓝牙通信流程中,中央设备(Central)通过 CBCentralManager 扫描、连接外设(Peripheral),外设通过 CBPeripheral 代理回调向上层传递原始字节流。数据以 Data(即 NSData 的 Swift 桥接类型)形式交付,但蓝牙传输协议不具备内置帧边界,应用层需自行处理粘包、分包和校验。本环节是 Keychain 加密的数据入口,预处理质量直接影响签名验证与解密成功率。

核心回调链路:
CBCentralManagerDelegate → didDiscover → didConnect → CBPeripheralDelegate → didDiscoverServices → didDiscoverCharacteristicsFor → didUpdateValueFor → 应用层 Data 缓冲区 → 边界重组

2.2 实现流程

  1. 初始化 CBCentralManager:指定后台运行队列,设置代理。
  2. 扫描外设:调用 scanForPeripherals(withServices:options:),过滤指定 Service UUID。
  3. 连接外设:centralManager(_:didDiscover:advertisementData:rssi:) 回调中调用 connect(_:options:)。
  4. 发现服务和特征:连接成功后,调用 discoverServices(_:),再在 didDiscoverServices 中对每个 Service 调用 discoverCharacteristics(_:for:)。
  5. 订阅通知:对需要接收数据的 Characteristic 调用 setNotifyValue(true, for:)。
  6. 接收数据:在 didUpdateValueFor characteristic 回调中提取 characteristic.value(类型 Data?)。
  7. 数据转换:将 Data 转为可操作的 NSData(Swift 中直接桥接)或 [UInt8] 以便手动解析。
  8. 字节流边界处理:维护可变缓冲区,按自定义协议头(长度字段+分隔符)提取完整报文。

2.3 代码 / API 调用

2.3.1 CBCentralManager 初始化与扫描

import CoreBluetooth

class BluetoothManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
    var centralManager: CBCentralManager!
    var targetPeripheral: CBPeripheral?
    var receiveBuffer = Data()

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: .main)
    }
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            central.scanForPeripherals(withServices: [CBUUID(string: "YOUR-SERVICE-UUID")], options: nil)
        }
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
        targetPeripheral = peripheral
        peripheral.delegate = self
        central.stopScan()
        central.connect(peripheral, options: nil)
    }
}

2.3.2 连接后订阅特征与接收数据

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    peripheral.discoverServices([CBUUID(string: "YOUR-SERVICE-UUID")])
}

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    guard let service = peripheral.services?.first else { return }
    peripheral.discoverCharacteristics([CBUUID(string: "YOUR-CHARACTERISTIC-UUID")], for: service)
}

func peripheral(_ peripheral: CBPeripheral,
                didDiscoverCharacteristicsFor service: CBService,
                error: Error?) {
    guard let char = service.characteristics?.first else { return }
    peripheral.setNotifyValue(true, for: char)
}

func peripheral(_ peripheral: CBPeripheral,
                didUpdateValueFor characteristic: CBCharacteristic,
                error: Error?) {
    guard let newData = characteristic.value else { return }
    // Data 转 NSData(隐式桥接)
    let nsData = newData as NSData
    // 或转字节数组用于边界处理
    let bytes = [UInt8](newData)
    // 追加到缓冲区
    receiveBuffer.append(newData)
    // 尝试从缓冲区提取完整报文
    processReceivedBuffer()
}

2.3.3 字节流边界处理示例

private func processReceivedBuffer() {
    // 假设协议:前4字节小端序总长度(含自身),后续为负载
    let headerLength = 4
    while receiveBuffer.count >= headerLength {
        let totalLength = receiveBuffer.withUnsafeBytes { $0.load(as: UInt32.self) }
        guard totalLength > 0 && totalLength <= 4096 else {
            // 非法长度,清空缓冲区
            receiveBuffer.removeAll()
            return
        }
        if receiveBuffer.count >= Int(totalLength) {
            let packet = receiveBuffer.prefix(Int(totalLength))
            // packet 是一个完整报文,可送入解密或 Keychain 存储
            handleCompletePacket(Data(packet))
            receiveBuffer.removeFirst(Int(totalLength))
        } else {
            break // 等待更多数据
        }
    }
}

2.4 调试与常见错误

错误场景表象排查方向
蓝牙权限未开启centralManagerDidUpdateState 状态为 .unauthorized检查 Info.plist 中 NSBluetoothAlwaysUsageDescription 或 NSBluetoothPeripheralUsageDescription;iOS 13+ 必须包含前者。
特征未订阅didUpdateValueFor 从未触发确认在 didDiscoverCharacteristicsFor 后调用了 setNotifyValue(true, for:),且外设支持通知。
数据乱序或丢失报文长度字段超出预期,缓冲区始终无法重组检查外设发送间隔是否过大;考虑使用带应答的写入(WRITE_TYPE_WITH_RESPONSE)或启用流控制。
边界处理死循环性能下降、卡死确保在 while 循环中有正确的 break 条件,避免 removeFirst 后缓冲区长度错误。
主线程阻塞UI 卡顿、回调延迟数据处理应放在后台队列,只将结果回调至主线程;可使用 centralManager 的 queue 参数指定串行后台队列。
调试利器: 在 didUpdateValueFor 中打印 newData.count 和每次追加后的缓冲区长度,可快速定位粘包或遗漏。

2.5 性能与稳定性优化

  • 缓冲区上限:设置 receiveBuffer 最大容量(如 5MB),超出则清理旧数据,防止内存泄漏。
  • 批量处理:若单次回调携带大量数据(常见于 OTA 升级),可使用 autoreleasepool 或 withExtendedLifetime 临时持有 Data,避免因 ARC 延迟释放导致峰值内存。
  • 队列切换:所有 CoreBluetooth 回调默认在 centralManager 初始化时指定的队列上执行。将预处理(边界重组、解密准备)放在同一个串行队列,避免多线程竞争。
  • 重连策略:在 centralManager(_:didDisconnectPeripheral:error:) 中自动重连,并重置缓冲区,避免脏数据。
  • 幂等处理:对重复收到的相同数据包(外设可能重传)进行去重,可通过报文序列号或时间戳实现。
注意: 不要在主线程中执行大量 Data 拷贝或转换操作。建议使用 data.withUnsafeBytes 直接访问底层字节,避免中间对象分配。
CoreBluetooth 数据接收与预处理流程 外设 (Peripheral) CBCentralManager 连接 & 发现服务特征 订阅通知 setNotifyValue didUpdateValueFor Data → NSData / [UInt8] 可变缓冲区 边界重组 (长度字段+分隔符) 完整报文 → Keychain 说明:数据经 4 步回调到达应用层,缓冲区通过长度字段+循环提取完整报文。 确保每次 didUpdateValueFor 后调用 processReceivedBuffer 防止缓冲区溢出。