CoreBluetooth 数据接收与预处理:CBCentralManager / CBPeripheral 回调、Data 转 NSData、字节流边界处理
2.1 原理说明
iOS 蓝牙通信流程中,中央设备(Central)通过 CBCentralManager 扫描、连接外设(Peripheral),外设通过 CBPeripheral 代理回调向上层传递原始字节流。数据以 Data(即 NSData 的 Swift 桥接类型)形式交付,但蓝牙传输协议不具备内置帧边界,应用层需自行处理粘包、分包和校验。本环节是 Keychain 加密的数据入口,预处理质量直接影响签名验证与解密成功率。
核心回调链路:
CBCentralManagerDelegate → didDiscover → didConnect → CBPeripheralDelegate → didDiscoverServices → didDiscoverCharacteristicsFor → didUpdateValueFor → 应用层 Data 缓冲区 → 边界重组
CBCentralManagerDelegate → didDiscover → didConnect → CBPeripheralDelegate → didDiscoverServices → didDiscoverCharacteristicsFor → didUpdateValueFor → 应用层 Data 缓冲区 → 边界重组
2.2 实现流程
- 初始化 CBCentralManager:指定后台运行队列,设置代理。
- 扫描外设:调用
scanForPeripherals(withServices:options:),过滤指定 Service UUID。 - 连接外设:
centralManager(_:didDiscover:advertisementData:rssi:)回调中调用connect(_:options:)。 - 发现服务和特征:连接成功后,调用
discoverServices(_:),再在didDiscoverServices中对每个 Service 调用discoverCharacteristics(_:for:)。 - 订阅通知:对需要接收数据的 Characteristic 调用
setNotifyValue(true, for:)。 - 接收数据:在
didUpdateValueFor characteristic回调中提取characteristic.value(类型Data?)。 - 数据转换:将
Data转为可操作的NSData(Swift 中直接桥接)或[UInt8]以便手动解析。 - 字节流边界处理:维护可变缓冲区,按自定义协议头(长度字段+分隔符)提取完整报文。
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 直接访问底层字节,避免中间对象分配。