Core Bluetooth是苹果官方提供的蓝牙低功耗(BLE)开发框架,智能手环、心率带、体温计等可穿戴设备几乎都基于BLE协议与iPhone通信。要在iOS端实现一套完整的手环交互流程,通常需要经历扫描、连接、服务发现、特征值读写、订阅通知、断线重连这几个阶段。很多开发者能跑通官方Demo,但真正对接一款商用智能手环时,往往卡在配对绑定和私有协议解析这两步。这篇文章就把整个链路掰开揉碎,配合Swift代码逐一说明。

一、扫描与连接:不要只靠Local Name过滤
使用Core Bluetooth的第一步是初始化CBCentralManager,并在它的代理回调中监听蓝牙状态。只有当状态变为poweredOn之后才能开始扫描,否则调用scanForPeripherals不会有任何效果。这一点在真机测试时要特别注意,模拟器不支持蓝牙,必须用真机调试。
class BandManager: NSObject, CBCentralManagerDelegate {
var centralManager: CBCentralManager!
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
// 扫描时通过服务UUID过滤,比只靠名称更可靠
centralManager.scanForPeripherals(
withServices: [CBUUID(string: "FEE7")],
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
)
}
}
}扫描过滤有两种方式:一是按广播包中携带的Service UUID过滤,二是扫描全部设备再根据advertisementData中的kCBAdvDataLocalName字段做名称匹配。实际项目中强烈推荐前者,因为大量手环出于省电考虑,广播间隔设置得比较长(比如1秒以上),名称字段有时还会被截断,纯靠名称匹配很容易扫不到目标设备。而按服务UUID过滤可以让系统在底层帮你做匹配,效率高得多。
扫描到目标设备后调用connect发起连接。连接超时默认大约10秒,超时后系统会回调didFailToConnect。这里有个常见坑:重复调用connect不会叠加连接请求,建议在失败回调里做有限次数的重试,而不是无限循环重试,否则在设备没电或被带离范围时会持续消耗电量。
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi: NSNumber) {
targetPeripheral = peripheral
peripheral.delegate = self
centralManager.connect(peripheral, options: nil)
}
func centralManager(_ central: CBCentralManager,
didConnect peripheral: CBPeripheral) {
peripheral.discoverServices(nil)
}二、配对绑定的正确理解:加密触发时机是关键
很多开发者对"配对"存在误解,认为iOS连接蓝牙设备时必须先像Android那样弹窗配对。实际上,BLE的配对(Pairing)是由访问加密特征值触发的,而不是连接动作本身。当你读取或写入一个带有"加密读取"(C encryption read)权限的特征值时,iOS才会与手环走配对流程,此时系统会弹出配对确认框,用户确认后双方完成密钥交换并绑定。
实际的手环产品通常会定义一个"绑定命令":App通过某个可写特征值写入一条绑定指令(可能携带用户ID或随机数),手环校验后返回成功应答,同时在固件侧把这部手机记为已绑定设备。这种应用层绑定配合系统级配对一起使用,才能实现"换一部手机需要重新解绑"的体验。需要注意,iOS不提供直接解除蓝牙绑定的API,只能引导用户去系统设置的蓝牙列表里点击"忽略此设备",App内可以通过提示文案引导用户完成这一步。
另一个要点是配对后iOS会自动缓存手环的服务和特征值,这就是所谓的"系统缓存"。如果你在开发调试中修改了固件的服务表,手机上可能读到的还是旧结构。解决办法很简单:在系统蓝牙设置中忽略该设备,或者重启手机蓝牙开关。这个问题在联调阶段出现频率极高,值得提前知道。
三、电量读取:标准Battery Service的用法
电量读取是BLE里最标准化的部分。蓝牙联盟规定了标准的电量服务,UUID为0x180F,其下只有一个电量特征值0x2A19,值为0到100的整数,单位是百分比。读取代码如下:
let batteryServiceUUID = CBUUID(string: "180F")
let batteryLevelUUID = CBUUID(string: "2A19")
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
for service in peripheral.services ?? [] {
if service.uuid == batteryServiceUUID {
peripheral.discoverCharacteristics([batteryLevelUUID], for: service)
}
}
}
func peripheral(_ peripheral: CBPeripheral,
didDiscoverCharacteristicsFor service: CBService,
error: Error?) {
for characteristic in service.characteristics ?? [] {
if characteristic.uuid == batteryLevelUUID {
// 读取当前电量值
peripheral.readValue(for: characteristic)
}
}
}
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
if characteristic.uuid == batteryLevelUUID,
let data = characteristic.value,
let level = data.first {
print("手环电量:\(level)%")
}
}需要注意电量特征值通常没有Notify权限,也就是说手环电量下降不会主动推送,只能靠App主动轮询。轮询频率建议控制在几分钟一次,过于频繁的读取会增加连接事件,反而加快手环耗电。也有厂商会在自定义服务里再实现一个带Notify的电量特征,这就要看各家私有协议文档的定义了。
四、运动数据同步:私有协议的订阅与解析
手环的运动数据(步数、距离、卡路里、睡眠等)几乎都走厂商自定义服务和特征值,通常以Notify方式推送。App需要先调用setNotifyValue(true, for:)`订阅该特征值,订阅成功后,手环每产生新数据就会通过didUpdateValueFor回调推送上来。数据格式完全由厂商定义,常见的做法是小端序的多字段打包,比如一个数据包里前两字节是步数,接着两字节是卡路里,最后是时间戳。
// 订阅运动数据特征值
peripheral.setNotifyValue(true, for: sportCharacteristic)
// 解析推送上来的数据包
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard let data = characteristic.value else { return }
// 假设协议:前2字节步数,2字节卡路里,4字节时间戳(小端序)
var steps: UInt16 = 0
var calories: UInt16 = 0
var timestamp: UInt32 = 0
(data as NSData).getBytes(&steps, range: NSRange(location: 0, length: 2))
(data as NSData).getBytes(&calories, range: NSRange(location: 2, length: 2))
(data as NSData).getBytes(×tamp, range: NSRange(location: 4, length: 4))
print("步数:\(steps),卡路里:\(calories),时间戳:\(timestamp)")
}单包BLE数据负载最大只有20字节(在不开启MTU协商的情况下),所以一次全天运动数据同步往往要拆成多个包传输。这就要求App端实现分包重组逻辑:协议里一般会有序号字段或起始标志位,收到"传输开始"标志后建立缓冲区,把后续包依次拼接,直到收到"传输结束"标志再统一做CRC校验并解析。如果校验失败,应该通过写入某个控制特征值向手环请求重传,而不是直接丢弃数据。
同步时机也有讲究。比较稳妥的方案是App进入前台或手环重新连接成功时,先通过写入控制命令查询"是否有未同步数据",手环返回数据量后,再按批次拉取。避免每次连接都全量同步,既浪费传输时间也耗电。
五、断线重连与后台模式的注意事项
手环类产品对连接稳定性要求很高。Core Bluetooth提供了两种重连方式:一是retrievePeripherals(withIdentifiers:),通过首次连接时保存的peripheral.identifier(UUID)重连已知设备;二是retrieveConnectedPeripherals(withServices:),获取已被系统或其他App连接的设备。推荐的做法是在绑定成功后把identifier存入UserDefaults,之后每次启动App直接尝试重连,连不上再退化到扫描。
func centralManager(_ central: CBCentralManager,
didDisconnectPeripheral peripheral: CBPeripheral,
error: Error?) {
// 断开后自动重连
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
central.connect(peripheral, options: nil)
}
}如果希望App退到后台后仍能接收手环数据,必须在Xcode的Capabilities中勾选Uses Bluetooth LE accessories后台模式。开启后,App在后台期间可以保持已建立的连接并继续接收Notify推送,但注意后台状态下不能主动发起扫描去发现新设备。另外,iOS对后台运行时间有严格限制,长时间纯后台重连建议配合CoreBluetooth的State Restoration机制,在App被系统杀掉后恢复蓝牙状态,这样才能真正做到"手环一靠近,数据自动同步"的体验。
整体来看,用Core Bluetooth对接智能手环的技术核心在于三块:扫描连接的状态机管理、配对绑定的触发时机、私有协议的分包解析。把这三块封装成一个独立的管理类,处理好各种异常回调,一套可复用的手环通信模块就成型了。实际项目中还建议加入连接超时的用户提示和协议版本号协商,为后续固件升级留出扩展空间。
Core BluetoothiOS蓝牙开发智能手环数据同步修改时间:2026-09-14 09:31:06