免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

iOS蓝牙开发实战:从权限配置到数据读写,避开CoreBluetooth的坑

iOS蓝牙开发实战:从权限配置到数据读写,避开CoreBluetooth的坑 简介这是一份面向iOS开发者、尤其是初涉蓝牙低功耗BLE编程人群的实战示例资源围绕苹果Core Bluetooth框架展开帮助读者理解CBCentralManager、CBPeripheral、CBService、CBCharacteristic及GATT协议等核心概念解决从扫描设备到连接、读写特征、订阅通知的完整流程问题。压缩包共39个文件约60KB以9个.m实现文件与6个.h头文件为主体配合12个png示意图、storyboard界面文件、plist配置及工程文件构成一个可直接运行的蓝牙Demo工程便于对照代码理解各代理方法的调用时机。目前已有137人学习下载。通过该示例读者可掌握中央管理者扫描与连接外设、发现服务与特征、数据交换与状态回调的写法并了解iOS 13及以上蓝牙权限申请与App审核注意事项为在自有应用中集成BLE功能提供可复用的参考骨架。1. 从零手搓 iOS 蓝牙开发为什么你的第一行代码就卡在了权限上很多人第一次碰 iOS 蓝牙开发场景都差不多手头有个心率计、一个蓝牙温湿度传感器或者一块自定义的 BLE 模块想用 iPhone 连上去读数据。打开 Xcode新建工程搜到CoreBluetooth然后发现——代码写完了真机跑起来centralManagerDidUpdateState死活不回调或者扫描不到任何设备。这不是玄学是 iOS 蓝牙开发里最典型的第一道坎权限、状态机、后台模式三件事没对齐后面全白搭。这篇笔记面向的是想真正把 iOS 蓝牙开发跑通的开发者不管你是刚接触 CoreBluetooth 的新手还是被各种回调顺序折磨过的老手。我会按「先立住概念 → 再动手复现 → 最后排坑」的路径把中心设备、外设、服务、特征值这条链路拆开讲清楚每一步都落到可抄的代码和可调的参数上。读完你应该能做到用一台真机扫描到周边 BLE 设备连上读写特征值并且知道出问题时该看哪里。2. CoreBluetooth 的角色模型中心设备与外设到底谁在管谁2.1 先分清 Central 和 Peripheral别把两端写成一锅粥CoreBluetooth 的模型其实很干净但新手最容易混的地方在于一台 iPhone 既可以当中心设备Central也可以当外设Peripheral而这两套 API 是分开的。你做「手机连传感器」这类需求手机就是 Central传感器是 Peripheral。反过来如果你想让手机被别的设备发现那手机要跑CBPeripheralManager。关键对象就四个CBCentralManager中心设备管理器负责扫描、连接。CBPeripheral代表一个被发现的外设连接后用它来发现服务。CBService外设对外暴露的服务一个外设可以有多个服务。CBCharacteristic服务下的特征值真正读写数据的地方。数据流向是Central 扫描 → 发现 Peripheral → 连接 → 发现 Services → 发现 Characteristics → 读/写/订阅。这条链路每一步都是异步回调顺序不能乱乱了就是「扫描不到」「连上但读不到数据」。2.2 权限配置没有这两行真机上永远回调不到 poweredOn很多人代码没问题问题出在Info.plist。iOS 13 之后蓝牙权限必须显式声明否则CBCentralManager初始化后状态一直是unauthorized你连poweredOn都等不到。!-- Info.plist 里必须加这两项 -- keyNSBluetoothAlwaysUsageDescription/key string需要蓝牙连接设备以读取传感器数据/string keyNSBluetoothPeripheralUsageDescription/key string需要蓝牙连接设备以读取传感器数据/string逻辑说明NSBluetoothAlwaysUsageDescription是 iOS 13 必须的NSBluetoothPeripheralUsageDescription是旧版本兼容。描述文案会出现在系统弹窗里写清楚用途否则审核也可能被拒。参数上没什么可调的但注意模拟器不支持蓝牙必须真机调试这一点没有后悔药。提示如果你在「设置 → 隐私 → 蓝牙」里找不到你的 App说明它从来没成功请求过权限先检查 plist 和初始化时机。3. 扫描、连接、发现服务把最小可跑链路搭起来3.1 初始化 CentralManager 并等待 poweredOn第一步不是扫描是等状态。CBCentralManager初始化后系统会异步回调当前蓝牙状态只有poweredOn才能开始扫描。import CoreBluetooth class BluetoothManager: NSObject, CBCentralManagerDelegate { var centralManager: CBCentralManager! override init() { super.init() // queue 传 nil 表示在主队列回调调试方便生产可传独立队列 centralManager CBCentralManager(delegate: self, queue: nil) } func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: print(蓝牙可用开始扫描) // 只扫描指定服务避免扫到一堆无关设备 centralManager.scanForPeripherals( withServices: [CBUUID(string: FFF0)], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] ) case .poweredOff: print(蓝牙未开启) case .unauthorized: print(权限未授权检查 Info.plist) default: print(状态\(central.state.rawValue)) } } }逻辑说明scanForPeripherals的withServices参数很关键传nil会扫描所有 BLE 设备耗电且杂乱传具体服务 UUID 只扫你关心的。CBCentralManagerScanOptionAllowDuplicatesKey设为false表示同一设备只回调一次设为true会持续回调适合做信号强度实时显示。参数上扫描本身没有超时需要你自己用DispatchQueue.main.asyncAfter控制扫描时长一般 5 到 10 秒足够。3.2 发现外设后连接并设置代理扫描到设备后didDiscover回调会给你CBPeripheral、广告数据、RSSI。这时候不要急着连先做过滤。func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { // 按名称过滤避免连到隔壁工位的设备 guard let name peripheral.name, name.contains(Sensor) else { return } print(发现设备\(name)信号\(RSSI)) // 持有 peripheral 引用否则会被释放 self.discoveredPeripheral peripheral self.discoveredPeripheral.delegate self centralManager.stopScan() centralManager.connect(peripheral, options: nil) } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print(已连接\(peripheral.name ?? 未知)) // 连接成功后立刻发现服务 peripheral.discoverServices([CBUUID(string: FFF0)]) }逻辑说明peripheral必须用强引用持有否则回调结束后对象释放后续代理全部失效这是血泪经验。connect没有超时参数连接失败会走didFailToConnect你要在那里做重试或提示。discoverServices传nil会发现所有服务但实际项目里建议传具体 UUID减少不必要回调。3.3 发现特征值并订阅通知服务发现后继续发现特征值。特征值分读、写、通知三种属性你要根据properties判断能做什么。func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { guard error nil else { return } peripheral.services?.forEach { service in // 只处理目标服务 if service.uuid CBUUID(string: FFF0) { peripheral.discoverCharacteristics(nil, for: service) } } } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { guard error nil else { return } service.characteristics?.forEach { characteristic in // 可读特征值直接读 if characteristic.properties.contains(.read) { peripheral.readValue(for: characteristic) } // 支持通知的特征值订阅 if characteristic.properties.contains(.notify) { peripheral.setNotifyValue(true, for: characteristic) } } } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard error nil, let data characteristic.value else { return } // 按你的协议解析数据这里以十六进制打印 let hex data.map { String(format: %02x, $0) }.joined() print(收到数据\(hex)) }逻辑说明discoverCharacteristics传nil会发现该服务下所有特征值实际项目建议传具体 UUID。setNotifyValue(true)是订阅通知之后设备主动推数据会走didUpdateValueFor。参数上readValue是一次性读取notify是持续推送按需选择。注意写数据分withResponse和withoutResponse前者有回调确认后者快但不可靠按设备协议选。4. 避坑与排查那些让蓝牙开发变成玄学的瞬间4.1 扫描不到设备先看权限再看服务 UUID现象真机运行centralManagerDidUpdateState回调了poweredOn但didDiscover一直不触发。原因最常见的是scanForPeripherals的withServices传了错误 UUID或者设备根本没广播那个服务。其次是权限弹窗被拒绝后状态变成unauthorized但代码里没处理。解决先把withServices改成nil试一次如果能扫到说明 UUID 写错了。再检查「设置 → 隐私 → 蓝牙」里你的 App 是否开启。最后确认外设确实在广播有些模块需要先配对或按键唤醒。4.2 连上就断peripheral 引用没持有现象didConnect回调了但马上走didDisconnectPeripheral或者后续服务发现没反应。原因CBPeripheral对象在didDiscover回调结束后被释放了你没有用属性强引用它。解决在类里声明var discoveredPeripheral: CBPeripheral?在didDiscover里赋值。这是新手翻车率最高的一条没有之一。4.3 后台收不到数据没开后台模式现象App 切到后台蓝牙连接还在但didUpdateValueFor不再回调。原因iOS 默认不允许后台蓝牙通信需要在 Xcode 的 Signing Capabilities 里勾选Background Modes下的Uses Bluetooth LE accessories。解决勾选后在Info.plist里会生成UIBackgroundModes数组包含bluetooth-central。注意后台扫描有限制withServices必须传具体 UUID不能传nil否则后台不回调。4.4 写数据失败特征值属性不对现象调用writeValue后didWriteValueFor返回错误或者根本没回调。原因你写的特征值不支持.write或.writeWithoutResponse或者你用了withResponse但特征值只支持withoutResponse。解决在didDiscoverCharacteristicsFor里打印characteristic.properties确认支持的类型。写的时候用对应的方法writeValue(data, for: characteristic, type: .withResponse)或.withoutResponse。4.5 数据解析乱码字节序和分包没处理现象收到的Data转成字符串是乱码或者数值对不上。原因BLE 数据是小端序且可能分多次回调你没有做粘包处理。解决用withUnsafeBytes按小端序解析或者用Data的subdata按协议长度截取。如果设备一次发 20 字节以上要自己做缓冲区拼接。5. 进阶技巧用 RSSI 做距离估算与连接稳定性优化5.1 RSSI 不是距离但能帮你判断「够不够近」很多人想用 RSSI 直接算距离实际上环境干扰太大只能做粗略分级。我一般把 RSSI 分成三档大于 -60 算近-60 到 -80 算中小于 -80 算远。在didDiscover里持续读取 RSSI配合CBCentralManagerScanOptionAllowDuplicatesKey: true可以做实时信号显示。// 扫描时允许重复回调才能持续拿到 RSSI centralManager.scanForPeripherals( withServices: [CBUUID(string: FFF0)], options: [CBCentralManagerScanOptionAllowDuplicatesKey: true] ) // 在 didDiscover 里做平滑处理避免跳动 private var rssiBuffer: [Int] [] func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { rssiBuffer.append(RSSI.intValue) if rssiBuffer.count 5 { rssiBuffer.removeFirst() } let avg rssiBuffer.reduce(0, ) / rssiBuffer.count print(平滑后 RSSI\(avg)) }逻辑说明AllowDuplicatesKey: true会让同一设备持续回调适合做信号监测但耗电增加扫描时间建议控制在 10 秒内。rssiBuffer做滑动平均减少瞬时波动。参数上缓冲区大小 5 到 10 都可以太大反应迟钝太小没效果。5.2 连接参数与重连策略BLE 连接间隔由外设决定iOS 不能直接改但你可以通过connect的 options 传CBConnectPeripheralOptionNotifyOnDisconnectionKey让系统在断开时弹通知。重连策略上我一般用指数退避第一次断开等 1 秒重连第二次 2 秒第三次 4 秒最多 30 秒。别用固定 1 秒死循环耗电且容易被系统限制。private var reconnectDelay: TimeInterval 1.0 func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { print(断开连接\(reconnectDelay) 秒后重连) DispatchQueue.main.asyncAfter(deadline: .now() reconnectDelay) { central.connect(peripheral, options: nil) self.reconnectDelay min(self.reconnectDelay * 2, 30) } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { reconnectDelay 1.0 // 连接成功重置 peripheral.discoverServices([CBUUID(string: FFF0)]) }逻辑说明reconnectDelay初始 1 秒每次断开翻倍上限 30 秒。连接成功重置为 1 秒。这样既保证快速恢复又避免频繁重连被系统杀。参数上上限可以根据场景调传感器类 30 秒够用实时控制类可以降到 10 秒。5.3 用 Charles 抓包验证 BLE 数据不行但可以用 Xcode 日志BLE 是底层协议Charles 抓不到。我一般用 Xcode 的 Console.app 过滤CoreBluetooth日志或者在代码里把每个回调的characteristic.value打成十六进制和设备的协议文档逐字节对。如果设备厂商给了调试工具先用厂商工具确认数据格式再在 iOS 端复现。这一步没有捷径但能省掉大量「为什么数据不对」的排查时间。最后说个习惯我每次接新 BLE 设备第一件事不是写业务逻辑而是写一个最小 Demo把扫描、连接、读、写、通知五个动作各跑一遍确认链路通了再往上堆功能。这个习惯帮我省了至少几十次「代码没问题但就是不通」的深夜调试。希望帮到你。本文还有配套的精品资源点击获取
返回列表