如何对CoreBluetooth无响应写入做速率限制 保障BLE OTA升级稳定
核心问题定位
你当前代码的丢包本质是:
- 采用
withoutResponse类型的特征写入时,CoreBluetooth不会为应用层提供写入完成的确认回调,系统内部仅维护一个有限大小的写入缓冲区 - 同步for循环会一口气把所有数据包提交到系统缓冲区,缓冲区填满后新提交的写入会被直接静默丢弃,无任何报错
- 原代码在循环执行完成后就触发complete回调,本质上没有等到所有报文真正完成链路传输,进度计算也完全不符合实际发送状态
无UI冻结、高可靠的最优实现思路
1. 采用系统原生无响应写入流控机制
这是苹果官方推荐的最可靠方案,比自实现延时方案的带宽利用率高30%-50%:
- 利用CoreBluetooth在iOS 11+提供的
peripheral.canWriteWithoutResponse属性,以及对应的代理回调方法peripheralIsReady(toWriteWithoutResponse:) - 系统会根据当前蓝牙链路的实际剩余带宽、设备缓冲区余量主动触发回调,完全不需要应用层猜测等待时机
- 所有触发逻辑都运行在CoreBluetooth的全局调度队列上,完全不会阻塞主线程和UI渲染
2. 实现滑动窗口发送队列
不要采用“发一个等一次回调”的单包发送模式(带宽利用率极低),通过固定大小的滑动窗口平衡传输速度和可靠性:
- 维护全局的待发送数据包数组、已发送计数、当前允许的最大在途包数(窗口大小,通常根据MTU协商值设置为5-20,匹配你20字节单包的设计)
- 收到系统的可写入回调时,一次性从待发送队列取出最多等于窗口大小的数据包提交写入
- 每次提交完成后更新进度,通过主队列回调刷新UI,避免跨线程更新UI的异常
3. 状态重置与异常兜底
- 外设断开、发送完成时,清空待发送队列、重置窗口计数和状态标记,防止内存泄漏和状态错乱
- 所有
writeValue调用完成后,不立刻触发完成回调,直到待发送队列为空、所有数据包都完成链路提交后,再触发完成闭包
修正后的参考实现代码
// 全局OTA发送状态维护 private var otaPacketQueue: [Data] = [] private var otaTotalPacketCount: Int = 0 private var otaSentPacketCount: Int = 0 private let otaSendWindowSize = 10 // 可根据实际设备接收能力调整 private var otaCompletionHandler: (() -> Void)? private func sendOTAUpgrade(data: [UInt8], complete: @escaping () -> Void) { // 启动前重置所有状态 otaPacketQueue = makePackets(size: 20, arr: data).map { Data($0) } otaTotalPacketCount = otaPacketQueue.count otaSentPacketCount = 0 otaCompletionHandler = complete // 触发首次发送 sendNextPacketsIfAvailable() } private func sendNextPacketsIfAvailable() { // 待发送队列清空,触发完成回调 guard !otaPacketQueue.isEmpty else { otaCompletionHandler?() otaCompletionHandler = nil return } guard let peripheral = connectedPeripheral else { return } // 系统缓冲区无可用空间,等待下一次可写入回调 guard peripheral.canWriteWithoutResponse else { return } // 按窗口大小批量取出待发送包 let sendCount = min(otaSendWindowSize, otaPacketQueue.count) for _ in 0..<sendCount { let packet = otaPacketQueue.removeFirst() peripheral.writeValue(packet, for: otcDataCharacteristic!, type: .withoutResponse) otaSentPacketCount += 1 // 切回主队列刷新UI,避免跨线程异常 DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.firmwareUpgradePercentComplete = Double(self.otaSentPacketCount)/Double(self.otaTotalPacketCount) } } } // 实现CoreBluetooth代理回调,响应系统可写入事件 func peripheralIsReady(toWriteWithoutResponse peripheral: CBPeripheral) { sendNextPacketsIfAvailable() }
避坑提醒
- 绝对不要用固定时长的延时(比如每20ms发一个包)做流控:这种实现无法适应设备距离、信号强度、链路干扰带来的带宽波动,要么因为延时过短依旧丢包,要么因为延时过长大幅拉长OTA升级时间
- 不要在主线程执行批量
writeValue调用:所有发送动作都要通过CoreBluetooth的回调被动触发,严禁在主线程做同步的批量提交,避免UI冻结 - 不要把
withResponse类型的写入作为常规OTA方案:这种类型的写入每发一个包都要等设备回ack,传输速度会慢3-5倍,仅适合在链路不稳定的场景做兜底
内容的提问来源于stack exchange,提问作者Conrad Scherb
相关产品推荐
相关产品推荐

