将蓝牙设备连接到 Apple Watch
Connect Bluetooth devices to Apple Watch
2021年6月8日
一句话判断
如果你在做运动/健康类 Apple Watch app,这场 Session 能帮你避开蓝牙开发的几乎所有坑——特别是后台保活和数据解析那部分。
这场 Session 讲了什么
Apple Watch 作为运动和健康数据的中心设备,越来越多地需要连接外部蓝牙配件:心率带、功率计、跑步机、血氧仪等。这场 Session 围绕 CoreBluetooth 在 watchOS 上的使用展开,从设备发现、连接管理到数据读取的完整链路都做了详细讲解。
Session 重点讨论了后台蓝牙连接的注意事项。Apple Watch 的电池容量有限,系统对后台蓝牙扫描有严格限制。你需要通过 Info.plist 中声明 bluetooth-central 后台模式,并且在扫描时使用特定的 service UUID 过滤,而不是广播扫描所有设备。
另外还涉及了蓝牙数据解析的最佳实践。不同厂商的心率带数据格式可能不同(有些用标准 HRM profile,有些用自定义 service),Session 建议优先使用 Bluetooth SIG 标准化的 profile,减少兼容性问题。
值得深挖的点
后台蓝牙扫描的能耗陷阱
watchOS 对后台蓝牙的限制比 iOS 更激进。如果你的 app 在后台发起无过滤的扫描(不指定 service UUID),系统会直接忽略这个请求,而且不会给你任何回调。正确做法是在 CBCentralManager.scanForPeripherals 时传入 services 参数,只扫描你关心的 service。这样系统可以在硬件层面做过滤,大幅降低功耗。
另一个容易踩的坑是连接后的 keep-alive。后台状态下,如果长时间没有数据传输,系统可能会断开 BLE 连接。建议设置一个合理的 connection interval,或者在 CBPeripheralDelegate 中处理断开重连逻辑。
多设备同时连接的管理策略
运动场景下可能同时连接心率带和功率计。每个 CBPeripheral 的 delegate 回调是异步的,如果多个设备同时推送数据,需要用串行队列(DispatchQueue)来保证数据处理的顺序性。Session 推荐用 OperationQueue 配合最大并发数为 1 来管理多个传感器数据的处理流程。
代码片段
// 指定 service UUID 的后台扫描
import CoreBluetooth
class BLEManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
var centralManager: CBCentralManager!
// 心率服务的标准 UUID
let heartRateServiceUUID = CBUUID(string: "180D")
func startScanning() {
// 只扫描心率服务,避免全量广播扫描
centralManager.scanForPeripherals(
withServices: [heartRateServiceUUID],
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
)
}
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi: NSNumber) {
// 发现设备后停止扫描,节省电量
centralManager.stopScan()
peripheral.delegate = self
centralManager.connect(peripheral)
}
}
// 解析标准心率数据
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard let data = characteristic.value else { return }
// 心率测量值特征 (0x2A37)
if characteristic.uuid == CBUUID(string: "2A37") {
var buffer = [UInt8](repeating: 0, count: data.count)
data.copyBytes(to: &buffer, count: data.count)
// 第一个字节的 bit 0 决定心率是 UINT8 还是 UINT16
let isUINT16 = (buffer[0] & 0x01) != 0
let heartRate: Int
if isUINT16 {
heartRate = Int(buffer[1]) | (Int(buffer[2]) << 8)
} else {
heartRate = Int(buffer[1])
}
print("当前心率: \(heartRate) bpm")
}
}
// 后台断连重连策略
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
// 后台断连后自动重连
// 指数退避策略,避免频繁重连消耗电量
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
central.connect(peripheral)
}
}
最佳实践
在 watchOS 上做蓝牙开发,一定要在真机上测试后台行为。模拟器不会触发后台扫描限制,很多 bug 只有在真机上才能复现。建议在 centralManagerDidUpdateState 回调中检查 .poweredOn 状态后再开始扫描。
对于运动类 app,考虑把蓝牙连接管理封装成一个独立的服务类,与 WorkoutSession 的生命周期绑定。运动开始时建立连接,运动结束后主动断开。不要在 app 启动时就建立蓝牙连接——Apple Watch 用户对电池消耗非常敏感。
还有什么值得关注
- watchOS 8 新增了对 Bluetooth 5.0 特性的更好支持,包括更大的广播数据包。
- 配合
HKObserverQuery可以在健康数据更新时触发蓝牙同步,而不需要轮询。 - 如果你只需要配对一次的简单场景,考虑用 Accessory Setup Kit 替代完整的 CoreBluetooth 流程,用户体验更好。