如何让蓝牙通信方法返回didUpdateValue回调的响应结果
解决蓝牙异步回调转同步返回的问题
因为CoreBluetooth的通信是异步回调模式(发送指令后通过didUpdateValue代理方法接收响应),要实现var batteryLevel = getBatteryLevel()这种同步写法,需要把异步回调转换为同步返回逻辑,下面提供两种实用方案:
方案一:使用 Async/Await(iOS 15+ 推荐)
利用Swift的异步等待特性,将代理回调包装成异步函数,写法更简洁且不会阻塞线程。
步骤1:定义指令枚举与等待逻辑
先定义一个枚举标记不同指令(用于区分响应归属),再用withCheckedThrowingContinuation包装等待响应的逻辑:
// 自定义指令枚举,对应不同的蓝牙命令 enum BluetoothCommand: CaseIterable { case getVersion case getBatteryLevel // 转成对应指令的Data var commandData: Data { switch self { case .getVersion: return Data([0x01]) case .getBatteryLevel: return Data([0x02]) } } // 从响应Data解析对应的指令类型 static func fromResponse(_ data: Data) -> BluetoothCommand? { guard let firstByte = data.first else { return nil } switch firstByte { case 0x81: return .getVersion case 0x82: return .getBatteryLevel default: return nil } } } class BluetoothManager: NSObject, CBPeripheralDelegate { private var connectedDevice: CBPeripheral? private var rxCharacteristic: CBCharacteristic? // 保存等待中的异步续体,关联指令类型 private var pendingContinuations: [BluetoothCommand: CheckedContinuation<Data, Error>] = [:] // 封装等待响应的异步方法 private func waitForResponse(for command: BluetoothCommand) async throws -> Data { return try await withCheckedThrowingContinuation { continuation in pendingContinuations[command] = continuation // 发送指令 connectedDevice?.writeValue(command.commandData, for: rxCharacteristic!, type: .withResponse) } }
步骤2:代理方法中触发异步续体
在didUpdateValue中解析响应,找到对应的续体并返回结果:
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard let responseData = characteristic.value, error == nil else { // 所有等待中的请求抛出错误 pendingContinuations.values.forEach { $0.resume(throwing: error ?? NSError(domain: "BluetoothError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应错误"])) } pendingContinuations.removeAll() return } guard let command = BluetoothCommand.fromResponse(responseData) else { pendingContinuations.values.forEach { $0.resume(throwing: NSError(domain: "BluetoothError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无效响应"])) } pendingContinuations.removeAll() return } // 触发对应指令的续体,返回响应数据 if let continuation = pendingContinuations.removeValue(forKey: command) { continuation.resume(returning: responseData) } }
步骤3:实现同步风格的业务方法
现在可以直接写异步方法,调用时用await即可:
// 获取电池电量的异步方法 func getBatteryLevel() async throws -> Int { let responseData = try await waitForResponse(for: .getBatteryLevel) // 解析响应数据(示例:取第一个字节作为电量百分比) guard let batteryByte = responseData.last else { throw NSError(domain: "BluetoothError", code: -3, userInfo: [NSLocalizedDescriptionKey: "解析失败"]) } return Int(batteryByte) } }
调用示例
Task { do { let batteryLevel = try await bluetoothManager.getBatteryLevel() print("当前电池电量:\(batteryLevel)%") } catch { print("获取电量失败:\(error.localizedDescription)") } }
方案二:使用 DispatchSemaphore(兼容 iOS 15 之前版本)
通过信号量阻塞当前线程,等待代理回调完成后释放信号量,实现同步返回。注意:不能在主线程调用,避免阻塞UI。
步骤1:定义线程安全的等待变量
class BluetoothManager: NSObject, CBPeripheralDelegate { private var connectedDevice: CBPeripheral? private var rxCharacteristic: CBCharacteristic? private let queue = DispatchQueue(label: "com.bluetooth.queue") // 线程安全队列 private var pendingSemaphores: [BluetoothCommand: DispatchSemaphore] = [:] private var latestResponse: Data? private var latestError: Error?
步骤2:实现同步业务方法
func getBatteryLevel() throws -> Int { let command = BluetoothCommand.getBatteryLevel let semaphore = DispatchSemaphore(value: 0) // 线程安全保存信号量 queue.sync { pendingSemaphores[command] = semaphore } // 发送指令 connectedDevice?.writeValue(command.commandData, for: rxCharacteristic!, type: .withResponse) // 等待信号量,设置5秒超时 let timeoutResult = semaphore.wait(timeout: .now() + 5) guard timeoutResult == .success else { queue.sync { pendingSemaphores.removeValue(forKey: command) } throw NSError(domain: "BluetoothError", code: -1, userInfo: [NSLocalizedDescriptionKey: "请求超时"]) } // 线程安全读取结果 return try queue.sync { if let error = latestError { self.latestError = nil throw error } guard let responseData = latestResponse else { throw NSError(domain: "BluetoothError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无响应数据"]) } self.latestResponse = nil guard let batteryByte = responseData.last else { throw NSError(domain: "BluetoothError", code: -3, userInfo: [NSLocalizedDescriptionKey: "解析失败"]) } return Int(batteryByte) } }
步骤3:代理方法中释放信号量
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { queue.sync { guard let responseData = characteristic.value, error == nil else { latestError = error ?? NSError(domain: "BluetoothError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应错误"]) pendingSemaphores.values.forEach { $0.signal() } pendingSemaphores.removeAll() return } guard let command = BluetoothCommand.fromResponse(responseData) else { latestError = NSError(domain: "BluetoothError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无效响应"]) pendingSemaphores.values.forEach { $0.signal() } pendingSemaphores.removeAll() return } latestResponse = responseData latestError = nil if let semaphore = pendingSemaphores.removeValue(forKey: command) { semaphore.signal() } } } }
调用示例
DispatchQueue.global().async { do { let batteryLevel = try bluetoothManager.getBatteryLevel() DispatchQueue.main.async { print("当前电池电量:\(batteryLevel)%") } } catch { DispatchQueue.main.async { print("获取电量失败:\(error.localizedDescription)") } } }
关键注意事项
- 指令与响应的绑定:必须通过指令枚举区分不同请求的响应,避免多请求时结果混乱。
- 超时处理:两种方案都要设置超时,防止无限等待。
- 线程安全:信号量方案必须用串行队列保护共享变量,避免多线程冲突。
- UI线程保护:信号量方案绝对不能在主线程调用,Async/Await方案可以在主线程的Task中调用,但耗时操作仍会被系统调度到后台。
内容的提问来源于stack exchange,提问作者Intra Preneur
相关产品推荐
相关产品推荐

