本文翻译整理自:Core Bluetooth Programming Guide(更新日期:2013-09-18
https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/AboutCoreBluetooth/Introduction.html#//apple_ref/doc/uid/TP40013257
文章目录
- 一、关于核心蓝牙
- 1、概览
- 中央设备和外围设备是核心蓝牙的关键参与者
- Core Bluetooth 简化常见的蓝牙任务
- iOS 应用程序状态影响蓝牙行为
- 遵循最佳实践来增强用户体验
- 2、如何使用本文档
- 3、也可以看看
- 二、核心蓝牙概述
- 1、蓝牙通信中的中央和外围设备及其作用
- 1.1 中心发现并连接正在广播的外围设备
- 1.2 外设的数据结构
- 1.3 中心设备探索外围设备上的数据并与之交互
- 2、如何表示中央设备、外围设备和外围数据
- 2.1 中心侧的物体
- 2.2 本地中央设备和远程外围设备
- 远程外设的数据由 CBService 和 CBCharacteristic 对象表示
- 外围设备端的对象
- 本地外围设备和远程中央设备
- 本地外设的数据由 CBMutableService 和 CBMutableCharacteristic 对象表示
- 三、执行常见中心角色任务
- 1、启动中央管理器
- 2、发现正在播放广告的外围设备
- 3、发现外围设备后进行连接
- 4、发现你所连接的外围设备的服务
- 5、发现服务的特征
- 6、检索特征值
- 6.1 读取特征值
- 6.2 订阅特征的值
- 7、写入特征值
- 四、执行常见的外围角色任务
- 1、启动外设管理器
- 2、设置您的服务和特性
- 2.1 服务和特性通过 UUID 进行标识
- 2.2 为自定义服务和特性创建自己的 UUID
- 2.3 构建您的服务和特征树
- 3、发布您的服务和特性
- 4、宣传您的服务
- 5、响应来自中央的读写请求
- 6、向订阅的 Central 发送更新的特征值
- 五、iOS 应用程序的核心蓝牙后台处理
- 1、前台应用
- 利用外围连接选项
- 2、核心蓝牙后台执行模式
- 2.1 以蓝牙为中心的后台执行模式
- 2.2 bluetooth-peripheral后台执行模式
- 3、明智地使用后台执行模式
- 4、在后台执行长期操作
- 4.1 国家保护和恢复
- 4.2 增加对国家保护和恢复的支持
- 选择加入国家保护和恢复
- 重新实例化您的中央和外围管理器
- 实施适当的恢复委托方法
- 更新您的初始化过程
- 六、与远程外围设备交互的最佳实践
- 1、注意无线电使用和功耗
- 1.1 仅在需要时扫描设备
- 1.2 仅在必要时指定CBCentralManagerScanOptionAllowDuplicatesKey选项
- 1.3 明智地探索外设数据
- 1.4 订阅经常变化的特性值
- 1.5 当您拥有所需的所有数据时,断开与设备的连接
- 2、重新连接到外围设备
- 2.1 检索已知外设列表
- 2.2 检索连接的外设列表
- 七、将本地设备设置为外围设备的最佳实践
- 1、广告注意事项
- 尊重广告数据的限制
- 仅在需要时为数据做广告
- 让用户决定何时发布广告
- 2、配置您的特征
- 配置您的特征以支持通知
- 需要配对连接才能访问敏感数据
一、关于核心蓝牙
Core Bluetooth 框架提供了 iOS 和 Mac 应用与配备低功耗蓝牙无线技术的设备通信所需的类。
例如,您的应用可以发现、探索和与低功耗外围设备(如心率监测器和数字恒温器)交互。
从 macOS 10.9 和 iOS 6 开始,Mac 和 iOS 设备还可以用作低功耗蓝牙外围设备,向其他设备(包括其他 Mac 和 iOS 设备)提供数据。
1、概览
低功耗蓝牙无线技术基于蓝牙 4.0 规范,该规范定义了一组用于低功耗设备之间通信的协议。
核心蓝牙框架是低功耗蓝牙协议栈的抽象。
也就是说,它隐藏了规范的许多底层细节,让您(开发人员)可以更轻松地开发与低功耗蓝牙设备交互的应用程序。
中央设备和外围设备是核心蓝牙的关键参与者
在低功耗蓝牙通信中,有两个关键参与者:中央设备和外围设备。
每个参与者都有不同的角色。
外围设备通常具有其他设备所需的数据。
中央设备通常使用外围设备提供的信息来完成某些任务。
例如,配备低功耗蓝牙技术的数字恒温器可能会将房间的温度提供给 iOS 应用程序,然后该应用程序以用户友好的方式显示温度。
每个参与者在执行其角色时都会执行一组不同的任务。
外围设备通过无线广播其拥有的数据来表明其存在。
中央设备会扫描附近可能有其感兴趣的数据的外围设备。
当中央设备发现这样的外围设备时,中央设备会请求连接到外围设备并开始探索和与外围设备的数据交互。
外围设备负责以适当的方式响应中央设备。
相关章节: 核心蓝牙概述
Core Bluetooth 简化常见的蓝牙任务
Core Bluetooth 框架从蓝牙 4.0 规范中抽象出底层细节。
因此,您需要在应用中实现的许多常见低功耗蓝牙任务都得到了简化。
如果您正在开发一款实现中央角色的应用,Core Bluetooth 可让您轻松发现和连接外围设备,以及探索和与外围设备的数据交互。
此外,Core Bluetooth 可让您轻松设置本地设备以实现外围设备角色。
相关章节: 执行常见的中心角色任务、执行常见的外围角色任务
iOS 应用程序状态影响蓝牙行为
当您的 iOS 应用处于后台或挂起状态时,其蓝牙相关功能会受到影响。
默认情况下,您的应用在后台或挂起状态下无法执行低功耗蓝牙任务。
也就是说,如果您的应用需要在后台执行低功耗蓝牙任务,您可以声明它支持一种或两种核心蓝牙后台执行模式(一种用于中央角色,一种用于外围角色)。
即使您声明了其中一种或两种后台执行模式,某些蓝牙任务在应用处于后台时也会以不同的方式运行。
您需要在设计应用时考虑到这些差异。
即使支持后台处理的应用也可能被系统随时终止,以便为当前前台应用释放内存。
从 iOS 7 开始,Core Bluetooth 支持保存中央和外围管理器对象的状态信息,并在应用启动时恢复该状态。
您可以使用此功能来支持涉及蓝牙设备的长期操作。
相关章节: iOS 应用的核心蓝牙后台处理
遵循最佳实践来增强用户体验
Core Bluetooth 框架可让您的应用控制许多常见的低功耗蓝牙事务。
遵循最佳实践,以负责任的方式利用这一级别的控制并增强用户体验。
例如,在实现中央或外围角色时,您执行的许多任务都使用设备的板载无线电通过无线方式传输信号。
由于设备的无线电与其他形式的无线通信共享,并且无线电的使用会对设备的电池寿命产生不利影响,因此在设计应用时,请务必尽量减少无线电的使用量。
相关章节: 与远程外围设备交互的最佳实践、将本地设备设置为外围设备的最佳实践
2、如何使用本文档
如果您从未使用过 Core Bluetooth 框架,或者不熟悉基本的低功耗蓝牙概念,请完整阅读本文档。
在Core Bluetooth 概述中,您将了解本书其余部分需要了解的关键术语和概念。
理解关键概念后,请阅读执行常见的中央角色任务,了解如何开发应用以在本地设备上实现中央角色。
同样,要了解如何开发应用以在本地设备上实现外围角色,请阅读执行常见的外围角色任务。
为了确保您的应用程序运行良好并遵循最佳实践,请阅读后面的章节:iOS 应用程序的核心蓝牙后台处理、与远程外围设备交互的最佳实践以及将本地设备设置为外围设备的最佳实践。
3、也可以看看
蓝牙特别兴趣小组 (SIG) 官方网站提供了有关蓝牙低功耗无线技术的权威信息。
您还可以在那里找到蓝牙 4.0 规范。
如果您正在设计使用低功耗蓝牙技术与 Apple 产品(包括 Mac、iPhone、iPad 和 iPod touch 型号)通信的硬件配件,请阅读Apple 产品的蓝牙配件设计指南。
如果您的蓝牙配件(通过低功耗蓝牙链接连接到 iOS 设备)需要访问 iOS 设备上生成的通知,请阅读*Apple 通知中心服务 (ANCS) 规范*。
二、核心蓝牙概述
Core Bluetooth 框架可让您的 iOS 和 Mac 应用与低功耗蓝牙设备通信。
例如,您的应用可以发现、探索低功耗外围设备并与之交互,例如心率监测器、数字恒温器,甚至其他 iOS 设备。
该框架是蓝牙 4.0 规范的抽象,用于低功耗设备。
也就是说,它隐藏了规范的许多底层细节,让您(开发人员)可以更轻松地开发与蓝牙低功耗设备交互的应用程序。
由于该框架基于规范,因此采用了规范中的一些概念和术语。
本章向您介绍了使用 Core Bluetooth 框架开始开发出色应用程序所需了解的关键术语和概念。
重要提示: 在 iOS 10.0 或之后链接的 iOS 应用必须在 Info.plist
文件中包含其需要访问的数据类型的使用描述键,否则会崩溃。
要专门访问蓝牙外围设备数据,它必须包含NSBluetoothPeripheralUsageDescription。
1、蓝牙通信中的中央和外围设备及其作用
所有蓝牙低功耗通信都涉及两个主要参与者:中央设备和外围设备。
基于某种传统的客户端-服务器架构,外围设备通常具有其他设备所需的数据。
中央设备通常使用外围设备提供的信息来完成某些特定任务。
例如,如图1-1所示,心率监测器可能具有有用的信息,您的 Mac 或 iOS 应用程序可能需要这些信息才能以用户友好的方式显示用户的心率。
图1-1 中心设备与周边设备
1.1 中心发现并连接正在广播的外围设备
外围设备以广告包的形式广播其拥有的部分数据。
广告包是一小包数据,可能包含有关外围设备所提供的有用信息,例如外围设备的名称和主要功能。
例如,数字恒温器可能会广告称其提供房间的当前温度。
在低功耗蓝牙中,广告是外围设备表明其存在的主要方式。
另一方面,中央设备可以扫描和监听任何外围设备,这些外围设备正在广播它感兴趣的信息,如图1-2所示。
中央设备可以请求连接到它发现的任何广播外围设备。
图 1-2 广告和发现
1.2 外设的数据结构
连接到外围设备的目的是开始探索和与其提供的数据进行交互。
不过,在执行此操作之前,了解外围设备的数据结构会有所帮助。
外围设备可能包含一项或多项服务,或提供有关其连接信号强度的有用信息。
服务是一组数据和相关行为,用于实现设备(或该设备的部分)的功能或特性。
例如,心率监测器的一项服务可能是显示来自监测器心率传感器的心率数据。
服务本身由特性或包含的服务(即对其他服务的引用)组成。
特性提供有关外围设备的服务的更多详细信息。
例如,刚刚描述的心率服务可能包含一个特性,该特性描述设备的心率传感器的预期身体位置,另一个特性传输心率测量数据。
图 1-3说明了心率监测器服务和特性的一种可能结构。
图 1-3 外设的服务和特性
1.3 中心设备探索外围设备上的数据并与之交互
在中心成功与外围设备建立连接后,它可以发现外围设备提供的全部服务和特性(广告数据可能只包含可用服务的一小部分)。
中央设备还可以通过读取或写入外围设备服务的特征值来与其服务进行交互。
例如,您的应用可以从数字恒温器请求当前房间温度,也可以向恒温器提供房间温度的设定值。
2、如何表示中央设备、外围设备和外围数据
低功耗蓝牙通信中涉及的主要参与者和数据以简单、直接的方式映射到核心蓝牙框架上。
2.1 中心侧的物体
当您使用本地中央设备与远程外围设备交互时,您将在低功耗蓝牙通信的中央端执行操作。
除非您正在设置本地外围设备(并使用它来响应中央设备的请求),否则您的大多数蓝牙事务都将在中央端进行。
有关如何在应用中实现中心角色的信息,请参阅执行常见的中心角色任务和与远程外围设备交互的最佳实践
2.2 本地中央设备和远程外围设备
在中心端,本地中心设备由CBCentralManager
对象表示。
这些对象用于管理已发现或已连接的远程外围设备(由CBPeripheral
对象表示),包括扫描、发现和连接广播外围设备。
图 1-4显示了本地中心和远程外围设备在核心蓝牙框架中的表示方式。
图1-4 中心端的核心蓝牙对象
远程外设的数据由 CBService 和 CBCharacteristic 对象表示
当您与远程外围设备(由CBPeripheral
对象表示)上的数据进行交互时,您正在处理其服务和特性。
在核心蓝牙框架中,远程外围设备的服务由CBService
对象表示。
同样,远程外围设备服务的特性也由CBCharacteristic
对象表示。
图 1-5说明了远程外围设备的服务和特性的基本结构。
图 1-5 远程外围设备的服务和特性树
外围设备端的对象
从 macOS 10.9 和 iOS 6 开始,Mac 和 iOS 设备可以用作低功耗蓝牙外围设备,为其他设备(包括其他 Mac、iPhone 和 iPad 设备)提供数据。
在设置设备以实现外围设备角色时,您正在低功耗蓝牙通信的外围设备端执行操作。
本地外围设备和远程中央设备
在外围设备方面,本地外围设备由CBPeripheralManager
对象表示。
这些对象用于管理本地外围设备的服务和特性数据库中已发布的服务,并将这些服务通告给远程中央设备(由CBCentral
对象表示)。
外围设备管理器对象还用于响应来自这些远程中央设备的读写请求。
图 1-6显示了本地外围设备和远程中央设备在核心蓝牙框架中的表示方式。
图1-6 外围设备端的核心蓝牙对象
本地外设的数据由 CBMutableService 和 CBMutableCharacteristic 对象表示
当您设置本地外围设备(由CBPeripheralManager
对象表示)并与其数据交互时,您正在处理其服务和特性的可变版本。
在 Core Bluetooth 框架中,本地外围设备的服务由CBMutableService
对象表示。
同样,本地外围设备服务的特性也由CBMutableCharacteristic
对象表示。
图 1-7说明了本地外围设备的服务和特性的基本结构。
图 1-7 本地外围设备的服务和特性树
有关如何设置本地设备以实现外围设备角色的更多信息,请参阅执行常见外围设备角色任务和将本地设备设置为外围设备的最佳实践。
三、执行常见中心角色任务
在蓝牙低功耗通信中充当中央角色的设备会执行许多常见任务,例如发现并连接可用的外围设备,以及探索外围设备提供的数据并与之交互。
充当外围设备的设备还会执行许多常见但不同的任务,例如发布和通告服务,以及响应来自连接的中央设备的读取、写入和订阅请求。
在本章中,您将学习如何使用 Core Bluetooth 框架从中央端执行最常见的蓝牙低功耗任务类型。
后面的代码示例将帮助您开发应用程序以在本地设备上实现中央角色。
具体来说,您将学习如何:
- 启动中央管理器对象
- 发现并连接正在做广告的外围设备
- 连接到外围设备后探索其上的数据
- 向外设服务的特征值发送读写请求
- 订阅特征值,以便在特征值更新时收到通知
在下一章中,您将学习如何开发您的应用程序以在本地设备上实现外围设备角色。
本章中的代码示例简单而抽象;您可能需要进行适当的更改才能将它们纳入您的实际应用中。
与实现中心角色相关的更多高级主题(包括提示、技巧和最佳实践)将在后面的章节中介绍,即iOS 应用的核心蓝牙后台处理和与远程外围设备交互的最佳实践。
1、启动中央管理器
因为CBCentralManager
对象是本地中央设备的核心蓝牙面向对象表示,所以在执行任何低功耗蓝牙事务之前,需要分配并初始化中央管理器实例。
可以通过调用 initWithDelegate:queue:options:
方法来初始化中央管理器:
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
在此示例中,self
设置为接收任何中央角色事件的委托。
通过将调度队列指定为nil
,中央管理器将使用主队列调度中央角色事件。
当您创建中央管理器时,中央管理器会调用其委托对象的centralManagerDidUpdateState:
方法。
您必须实现此委托方法,以确保中央设备支持低功耗蓝牙并可供使用。
有关如何实现此委托方法的更多信息,请参阅 CBCentralManagerDelegate 协议参考。
2、发现正在播放广告的外围设备
初始化后,中央管理器的首要任务是发现外围设备。
如中央管理器发现并连接到正在广告的外围设备中所述,外围设备通过广告来表明自己的存在。
您的应用通过调用中央管理器的方法来发现附近正在广告的外围设备scanForPeripheralsWithServices:options:
:
[myCentralManager scanForPeripheralsWithServices:nil options:nil];
注意: 如果您指定第一个参数为 nil
,中央管理器将返回所有发现的外围设备,无论它们支持什么服务。
在实际应用中,您通常会指定一个CBUUID
对象数组,每个对象代表外围设备正在宣传的服务的通用唯一标识符 (UUID)。
当您指定服务 UUID 数组时,中央管理器仅返回宣传这些服务的外围设备,让您只扫描您可能感兴趣的设备。
UUID 以及代表它们的 CBUUID
对象在 Services and Characteristics Are Identified by UUIDs 中有更详细的讨论。
每次中央管理器发现外围设备时,它都会调用其委托对象的 centralManager:didDiscoverPeripheral:advertisementData:RSSI:
方法。
新发现的外围设备将作为CBPeripheral
对象返回。
如果您计划连接到已发现的外围设备,请保持对它的强引用,以便系统不会释放它。
以下示例显示了使用类属性来维护对已发现外围设备的引用的场景:
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData
RSSI:(NSNumber *)RSSI {
NSLog(@"Discovered %@", peripheral.name);
self.discoveredPeripheral = peripheral;
...
如果您希望连接多个设备,您可以保留已发现外围设备的列表。
无论如何,一旦您找到了所有想要连接的外围设备,请停止扫描其他设备以节省电量:
[myCentralManager stopScan];
3、发现外围设备后进行连接
在你发现一个你感兴趣的外围设备广告服务后,你可以通过调用中央管理器的connectPeripheral:options:
方法来请求与外围设备的连接,并命名你想要连接的发现的外围设备:
[myCentralManager connectPeripheral:peripheral options:nil];
如果连接请求成功,中央管理器将调用其委托对象的 centralManager:didConnectPeripheral:
方法。
在开始与外围设备交互之前,请设置其委托,以确保委托收到适当的回调:
- (void)centralManager:(CBCentralManager *)central
didConnectPeripheral:(CBPeripheral *)peripheral {
NSLog(@"Peripheral connected");
peripheral.delegate = self;
...
4、发现你所连接的外围设备的服务
与外围设备建立连接后,您可以探索其数据。
探索外围设备所提供的服务的第一步是发现其可用的服务。
由于外围设备可以通告的数据量有大小限制,您可能会发现外围设备提供的服务比其通告的服务多(在其通告数据包中)。
您可以通过调用外围设备的discoverServices:
方法来发现外围设备提供的所有服务,如下所示:
[peripheral discoverServices:nil];
注意: 在实际应用中,您通常不会将传入nil
作为参数,因为这样做会返回外围设备上可用的所有服务。
由于外围设备可能包含比您感兴趣的更多的服务,因此发现所有这些服务可能会浪费电池寿命并浪费时间。
相反,您通常会指定您已经知道您有兴趣发现的服务的 UUID,如明智地探索外围设备的数据中所示。
当发现指定的服务时,外围设备(您连接到的CBPeripheral
对象)会调用其委托对象的 peripheral:didDiscoverServices:
方法。
Core Bluetooth 会创建一个CBService
对象数组,每个对象对应一个在外围设备上发现的服务。
如下所示,您可以实现此委托方法来访问已发现服务数组:
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverServices:(NSError *)error {
for (CBService *service in peripheral.services) {
NSLog(@"Discovered service %@", service);
...
}
...
5、发现服务的特征
找到感兴趣的服务后,探索外围设备所提供的服务的下一步是发现该服务的所有特性。
发现服务的所有特性非常简单,只需调用外围设备的discoverCharacteristics:forService:
方法并指定相应的服务即可,如下所示:
NSLog(@"Discovering characteristics for service %@", interestingService);
[peripheral discoverCharacteristics:nil forService:interestingService];
注意: 在实际应用中,您通常不会将 nil
作为第一个参数传入,因为这样做会返回外围设备服务的所有特性。
由于外围设备服务可能包含比您感兴趣的特性多得多的特性,因此发现所有这些特性可能会浪费电池寿命并浪费时间。
相反,您通常会指定您已经知道您有兴趣发现的特性的 UUID。
当发现指定服务的特征时,外围设备会调用其委托对象的 peripheral:didDiscoverCharacteristicsForService:error:
方法。
Core Bluetooth 会创建一个CBCharacteristic
对象数组 — 每个发现的特征对应一个对象。
以下示例展示了如何实现此委托方法,以简单地记录发现的每个特征:
- (void)peripheral:(CBPeripheral *)peripheral
didDiscoverCharacteristicsForService:(CBService *)service
error:(NSError *)error {
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"Discovered characteristic %@", characteristic);
...
}
...
6、检索特征值
特征包含单个值,该值表示有关外围设备服务的信息。
例如,健康温度计服务的温度测量特征可能具有一个表示摄氏温度的值。
您可以通过直接读取或订阅特征来检索特征的值。
6.1 读取特征值
找到您感兴趣的服务的特性后,您可以通过调用外围设备的readValueForCharacteristic:
方法来读取特性的值,并指定适当的特性,如下所示:
NSLog(@"Reading value for characteristic %@", interestingCharacteristic);
[peripheral readValueForCharacteristic:interestingCharacteristic];
当您尝试读取特征值时,外围设备会调用 其委托对象的 peripheral:didUpdateValueForCharacteristic:error:
方法来检索该值。
如果成功检索到该值,您可以通过特征的 value 属性访问它,如下所示:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
NSData *data = characteristic.value;
// parse the data as needed
...
注意:properties
并非所有特性都是可读的。
您可以通过检查特性的属性是否包含 CBCharacteristicPropertyRead
常量 来确定特性是否可读。
如果您尝试读取不可读特性的值,peripheral:didUpdateValueForCharacteristic:error:
委托方法将返回适当的错误。
6.2 订阅特征的值
虽然使用 readValueForCharacteristic:
方法读取特征值 对于静态值来说很有效,但它并不是检索动态值的最有效方法。
通过订阅随时间变化的特征值(例如您的心率),可以检索这些值。
当您订阅特征值时,您会在值发生变化时收到来自外围设备的通知。
您可以通过调用外围设备的setNotifyValue:forCharacteristic:
方法来订阅您感兴趣的特性的值,并将第一个参数指定为YES
,如下所示:
[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
当您订阅(或取消订阅)特征值时,外围设备会调用其委托对象的 peripheral:didUpdateNotificationStateForCharacteristic:error:
方法。
如果订阅请求因任何原因失败,您可以实现此委托方法来访问错误原因,如以下示例所示:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error) {
NSLog(@"Error changing notification state: %@",
[error localizedDescription]);
}
...
注意:properties
并非所有特性都提供订阅。
您可以通过检查特性的属性是否包含 CBCharacteristicPropertyIndicate
或CBCharacteristicPropertyNotify
常量 来确定特性是否提供订阅。
成功订阅特征值后,外围设备会在值发生变化时通知您的应用。
每次值发生变化时,外围设备都会调用其委托对象的 peripheral:didUpdateValueForCharacteristic:error:
方法。
要检索更新后的值,您可以按照上面读取特征值中 所述的方式实现此方法。
7、写入特征值
有时写入特性的值是有意义的。
例如,如果您的应用与低功耗蓝牙数字恒温器交互,您可能希望为恒温器提供一个值来设置房间的温度。
如果特性的值是可写的,您可以通过调用外围设备的writeValue:forCharacteristic:type:
方法使用数据(NSData
的实例)写入其值,如下所示:
NSLog(@"Writing value for characteristic %@", interestingCharacteristic);
[peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic
type:CBCharacteristicWriteWithResponse];
当您写入特征值时,请指定要执行的写入类型。
在上面的示例中,写入类型为CBCharacteristicWriteWithResponse
,它指示外围设备通过调用其委托对象的 peripheral:didWriteValueForCharacteristic:error:
方法让您的应用知道写入是否成功。
您可以实现此委托方法来处理错误情况,如以下示例所示:
- (void)peripheral:(CBPeripheral *)peripheral
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {
if (error) {
NSLog(@"Error writing characteristic value: %@",
[error localizedDescription]);
}
...
相反,如果您将写入类型指定为CBCharacteristicWriteWithoutResponse
,则写入操作将尽最大努力执行,并且既不保证也不报告交付。
外围设备不会调用任何委托方法。
有关核心蓝牙框架支持的写入类型的更多信息,请参阅*CBPeripheral 类参考* 中的 CBCharacteristicWriteType
枚举。
注意: 特性可能仅支持某些类型的写入,或者根本不支持。
您可以通过检查特性的properties
属性是否为 CBCharacteristicPropertyWriteWithoutResponse
或 CBCharacteristicPropertyWrite
常量 之一来确定特性支持哪种类型的写入(如果有) 。
四、执行常见的外围角色任务
在上一章中,您学习了如何从中央端执行最常见的蓝牙低功耗任务类型。
在本章中,您将学习如何使用核心蓝牙框架从外围设备端执行最常见的蓝牙低功耗任务类型。
后面的基于代码的示例将帮助您开发应用程序以在本地设备上实现外围设备角色。
具体来说,您将学习如何:
- 启动外设管理器对象
- 在本地外围设备上设置服务和特性
- 将您的服务和特性发布到设备的本地数据库
- 宣传您的服务
- 响应来自连接的中央的读写请求
- 将更新的特征值发送给订阅的中心
本章中的代码示例简单而抽象;您可能需要进行适当的更改才能将它们合并到您的实际应用中。
与在本地设备上实现外围设备角色相关的更多高级主题(包括提示、技巧和最佳实践)将在后面的章节中介绍,即iOS 应用的核心蓝牙后台处理和将本地设备设置为外围设备的最佳实践。
1、启动外设管理器
在本地设备上实现外围设备角色的第一步是分配并初始化外围设备管理器实例(由CBPeripheralManager
对象表示)。
通过调用 CBPeripheralManager
类的 initWithDelegate:queue:options:
方法 来启动外围设备管理器,如下所示:
myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
在此示例中,self
设置为接收任何外围角色事件的委托。
当您将调度队列指定为nil
时,外围设备管理器将使用主队列调度外围角色事件。
当您创建外围设备管理器时,外围设备管理器会调用其委托对象的 peripheralManagerDidUpdateState:
方法。
您必须实现此委托方法,以确保支持低功耗蓝牙并可在本地外围设备上使用。
有关如何实现此委托方法的更多信息,请参阅*CBPeripheralManagerDelegate 协议参考*。
2、设置您的服务和特性
如图1-7所示,本地外围设备的服务和特性数据库以树状结构组织。
您必须以这种树状结构组织它们,才能在本地外围设备上设置服务和特性。
执行这些任务的第一步是了解如何识别服务和特性。
2.1 服务和特性通过 UUID 进行标识
外设的服务和特性由 128 位蓝牙专用 UUID 标识,这些 UUID 在核心蓝牙框架中以对象表示CBUUID
。
虽然蓝牙技术联盟 (SIG) 并未预定义所有用于标识服务或特性的 UUID,但蓝牙技术联盟已定义并发布了许多常用的 UUID,这些 UUID 已缩短为 16 位以方便使用。
例如,蓝牙技术联盟已预定义用于标识心率服务的 16 位 UUID 为 180D。
此 UUID 是从其等效的 128 位 UUID 0000180D-0000-1000-8000-00805F9B34FB 缩短而来,后者基于蓝牙 4.0 规范第 3 卷 F 部分第 3.2.1 节中定义的蓝牙基本 UUID。
CBUUID
类提供了工厂方法,让您在开发应用时更轻松地处理长 UUID。
例如,您无需在代码中传递心率服务的 128 位 UUID 的字符串表示形式,只需使用UUIDWithString
方法从服务的预定义 16 位 UUID 创建CBUUID
对象即可,如下所示:
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
当您从预定义的 16 位 UUID 创建CBUUID
对象时,Core Bluetooth 会使用蓝牙基本 UUID 预填充其余的 128 位 UUID。
2.2 为自定义服务和特性创建自己的 UUID
您可能拥有预定义蓝牙 UUID 无法识别的服务和特性。
如果是这样,您需要生成自己的 128 位 UUID 来识别它们。
使用命令行实用程序uuidgen
轻松生成 128 位 UUID。
首先,在终端中打开一个窗口。
接下来,对于需要使用 UUID 标识的每个服务和特性,uuidgen
在命令行中键入以接收唯一的 128 位值,该值以 ASCII 字符串的形式显示,并以连字符分隔,如下例所示:
$ uuidgen
71DA3FD1-7E10-41C1-B16F-4430B506CDE7
然后,您可以使用此 UUID 通过下面的UUIDWithString
方法创建一个CBUUID
对象,如下所示:
CBUUID *myCustomServiceUUID = [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
2.3 构建您的服务和特征树
获得服务和特征的 UUID(以CBUUID
对象表示)后,您可以创建可变服务和特征,并按照上述树状方式组织它们。
例如,如果您拥有特征的 UUID,则可以通过调用 CBMutableCharacteristic
类的 initWithType:properties:value:permissions:
方法 来创建可变特征,如下所示:
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID
properties: CBCharacteristicPropertyRead
value: myValue permissions:CBAttributePermissionsReadable];
创建可变特征时,可以设置其属性、值和权限。
您设置的属性和权限决定了特征的值是否可读或可写,以及连接的中心是否可以订阅特征的值。
在此示例中,特征的值设置为连接的中心可读。
有关可变特征支持的属性和权限范围的更多信息,请参阅*CBMutableCharacteristic 类参考*。
注意: 如果您为特征指定了一个值,则该值将被缓存,并且其属性和权限将被设置为可读。
因此,如果您需要特征的值是可写的,或者如果您希望该值在该特征所属的已发布服务的生命周期内发生变化,则必须将该值指定为nil
。
遵循这种方法可确保每当外围设备管理器从连接的中央设备收到读取或写入请求时,外围设备管理器都会动态处理并请求该值。
现在您已经创建了可变特征,您可以创建一个可变服务来关联该特征。
为此,请调用CBMutableService
类的 initWithType:primary:
方法,如下所示:
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
在此示例中,第二个参数设置为YES
,表示该服务是主要服务而非次要服务。
主要服务描述设备的主要功能,可以由其他服务包含(引用)。
次要服务描述仅在引用它的其他服务的上下文中相关的服务。
例如,心率监测器的主要服务可能是公开来自监测器心率传感器的心率数据,而次要服务可能是公开传感器的电池数据。
创建服务后,您可以通过设置服务的特征数组将特征与其关联,如下所示:
myService.characteristics = @[myCharacteristic];
3、发布您的服务和特性
构建服务和特性树后,在本地设备上实现外围角色的下一步是将它们发布到设备的服务和特性数据库中。
使用 Core Bluetooth 框架可以轻松执行此任务。
您可以调用CBPeripheralManager
类的 addService:
方法,如下所示:
[myPeripheralManager addService:myService];
当您调用此方法发布服务时,外设管理器会调用其委托对象的 peripheralManager:didAddService:error:
方法。
如果发生错误并且无法发布服务,请实现此委托方法来访问错误原因,如以下示例所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didAddService:(CBService *)service
error:(NSError *)error {
if (error) {
NSLog(@"Error publishing service: %@", [error localizedDescription]);
}
...
注意: 将服务及其任何相关特性发布到外围设备的数据库后,该服务会被缓存,您无法再对其进行更改。
4、宣传您的服务
当您将服务和特性发布到设备的服务和特性数据库后,您就可以开始将其中一些服务广告给可能正在监听的任何中心。
如以下示例所示,您可以通过调用 CBPeripheralManager
类的 startAdvertising:
方法 来广告您的某些服务,并传入广告数据的字典( NSDictionary
的一个实例):
[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :
@[myFirstService.UUID, mySecondService.UUID] }];
在本例中,字典中唯一的关键字 CBAdvertisementDataServiceUUIDsKey
需要 CBUUID
对象的数组(NSArray
的实例)作为值,该数组表示要播发的服务的UUID。您可以在广告数据字典中指定的可能密钥在 CBCentralManagerDelegate Protocol Reference 中的 Advertisement Data Retrieval Keys 中描述的常量中有详细说明。
也就是说,外围管理器对象只支持其中两个键:CBAdvertisementDataLocalNameKey
和 CBAdvernisementDataServiceUUIDsKey
。
当您开始在本地外围设备上发布某些数据时,外围设备管理器会调用其委托对象的peripheralManagerDidStartAdvertising:error:
方法。
如果发生错误并且无法发布您的服务,请实现此委托方法来访问错误原因,如下所示:
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral
error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
...
注意: 数据广告是“尽力而为”的,因为空间有限,而且可能会有多个应用同时进行广告。
有关更多信息,请参阅*CBPeripheralManager 类参考* 中的startAdvertising:
方法讨论。
当您的应用处于后台时,广告行为也会受到影响。 下一章“ iOS 应用的核心蓝牙后台处理”将讨论此主题。
一旦您开始发布数据,远程中心就能发现您并与您建立连接。
5、响应来自中央的读写请求
连接到一个或多个远程中心后,您可能会开始接收来自它们的读取或写入请求。
这样做时,请确保以适当的方式响应这些请求。
以下示例描述了如何处理此类请求。
当连接的 central 请求读取您的某个特征的值时,外设管理器会调用其委托对象的 peripheralManager:didReceiveReadRequest:
方法。
委托方法会以CBATTRequest
对象的形式向您传递请求,该对象具有许多可用于满足请求的属性。
例如,当您收到读取特征值的简单请求时,您从委托方法收到的CBATTRequest
对象的属性可用于确保设备数据库中的特征与远程中央在原始读取请求中指定的特征相匹配。
您可以开始实现此委托方法,如下所示:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
...
如果特征的 UUID 匹配,下一步是确保读取请求未要求从特征值范围之外的索引位置读取。
如以下示例所示,您可以使用 CBATTRequest
对象的offset
属性来确保读取请求未尝试读取正确范围之外的内容:
if (request.offset > myCharacteristic.value.length) {
[myPeripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
return;
}
假设请求的偏移量已经验证,现在将请求的特征属性值(默认值为nil
)设置为您在本地外围设备上创建的特征值,同时考虑到读取请求的偏移量:
request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset, myCharacteristic.value.length - request.offset)];
设置值后,响应远程中心以指示请求已成功完成。
通过调用CBPeripheralManager
类的 respondToRequest:withResult:
方法,返回请求(其值已更新)和请求的结果,如下所示:
[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
...
peripheralManager:didReceiveReadRequest:
调用委托方法时,仅调用respondToRequest:withResult:
方法一次。
注意: 如果特征的 UUID 不匹配,或者由于任何其他原因无法完成读取,则您不会尝试满足请求。
相反,您会立即调用respondToRequest:withResult:
方法并提供指示失败原因的结果。
有关您可以指定的可能结果的列表,请参阅*Core Bluetooth Constants Reference* 中的枚举。
处理来自连接的中心的写入请求也很简单。
当连接的中心发送请求以写入一个或多个特征的值时,外围设备管理器会调用其委托对象的peripheralManager:didReceiveWriteRequests:
方法。
这一次,委托方法以包含一个或多个CBATTRequest
对象的数组形式将请求传递给您,每个对象代表一个写入请求。
确保可以满足写入请求后,您可以写入特征的值,如下所示:
myCharacteristic.value = request.value;
尽管上面的例子没有证明这一点,但在写入特征值时请务必考虑请求的偏移属性。
就像响应读取请求一样,每次调用peripheralManager:didReceiveWriteRequests:
委托方法时,都要准确地调用responsdToRequest:withResult:
方法一次。
也就是说,responsedToRequest:withResult:
方法的第一个参数需要一个 CBATTRequest
对象,即使您可能已经从 peripheralManager:didReceiveWriteRequests:
委托方法接收到一个包含多个对象的数组。
您应该传入数组的第一个请求,如下所示:
[myPeripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];
注意: 将多个请求视为单个请求 — 如果任何单个请求无法满足,则不应满足其中任何一个请求。
相反,应立即调用 respondToRequest:withResult:
方法并提供指示失败原因的结果。
6、向订阅的 Central 发送更新的特征值
通常,连接的中心会订阅一个或多个特征值,如订阅特征值中所述。
当他们这样做时,你有责任在他们订阅的特征值发生变化时向他们发送通知。
以下示例描述了如何操作。
当连接的中心订阅你的某个特性的值时,外围设备管理器会调用其委托对象的 peripheralManager:central:didSubscribeToCharacteristic:
方法:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
NSLog(@"Central subscribed to characteristic %@", characteristic);
...
使用上述委托方法作为提示,开始发送中心更新值。
接下来,通过调用CBPeripheralManager
类的 updateValue:forCharacteristic:onSubscribedCentrals:
方法获取特征的更新值并将其发送给中央。
SData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];
当你调用此方法将更新的特征值发送给已订阅的 centrals 时,你可以在最后一个参数中指定要更新哪些 centrals。
如上例所示,如果你指定nil
,则所有已连接且已订阅的 centrals 都会被更新(并且任何已连接但未订阅的 centrals 都会被忽略)。
updateValue:forCharacteristic:onSubscribedCentrals:
方法返回一个布尔值,表示更新是否已成功发送到订阅的 centrals。
如果用于传输更新值的底层队列已满,则该方法返回NO
。
当传输队列中有更多空间可用时,外围设备管理器会调用peripheralManagerIsReadyToUpdateSubscribers:
其委托对象的方法。
然后,您可以实现此委托方法来重新发送值,再次使用 updateValue:forCharacteristic:onSubscribedCentrals:
方法。
注意: 使用通知向订阅的中心发送单个数据包。
也就是说,当你更新订阅的中心时,你应该通过一次调用 updateValue:forCharacteristic:onSubscribedCentrals:
方法,在单个通知中发送整个更新值。
根据特征值的大小,并非所有数据都可以通过通知传输。
如果发生这种情况,应通过调用CBPeripheral
类的 readValueForCharacteristic:
方法来处理这种情况,该方法可以检索整个值。
五、iOS 应用程序的核心蓝牙后台处理
对于iOS应用,了解应用是在前台还是后台运行至关重要。
应用在后台的行为必须与前台不同,因为系统资源在iOS设备上更有限。
有关iOS上后台操作的整体讨论,请参阅*应用编程指南中*的后台执行。
默认情况下,当您的应用处于后台或挂起状态时,许多常见的核心蓝牙任务(在中央和外围)都被禁用。
也就是说,您可以声明您的应用支持核心蓝牙后台执行模式,以允许您的应用从挂起状态唤醒,以处理某些与蓝牙相关的事件。
即使您的应用不需要全方位的后台处理支持,它仍然可以在重要事件发生时要求系统发出警报。
即使您的应用支持一种或两种核心蓝牙后台执行模式,它也不能永远运行。
在某个时候,系统可能需要终止您的应用以释放当前前台应用的内存——例如,导致任何活动或挂起的连接丢失。
从iOS7开始,核心蓝牙支持保存中央和外围管理器对象的状态信息,并在应用启动时恢复该状态。
您可以使用此功能支持涉及蓝牙设备的长期操作。
1、前台应用
与大多数iOS应用一样,除非您请求执行特定后台任务的权限,否则您的应用会在进入后台状态后不久转换到挂起状态。
在挂起状态下,您的应用无法执行蓝牙相关任务,也不会意识到任何蓝牙相关事件,直到它恢复到前台。
在中心端,仅前台应用程序——未声明支持两种核心蓝牙后台执行模式的应用程序——在后台或暂停时无法扫描和发现广告外围设备。
在外围端,广告被禁用,任何试图访问应用程序已发布服务之一的动态特征值的中心都会收到错误。
根据用例,这种默认行为会以多种方式影响您的应用。
例如,假设您正在与当前连接的外围设备上的数据进行交互。
现在假设您的应用移动到挂起状态(例如,因为用户切换到另一个应用)。
如果在您的应用挂起时与外围设备的连接丢失,您将不会意识到发生了任何断开连接,直到您的应用恢复到前台。
利用外围连接选项
仅前台应用程序处于挂起状态时发生的所有蓝牙相关事件都由系统排队,并仅在应用程序恢复到前台时才传递给应用程序。
也就是说,核心蓝牙提供了一种在某些中心角色事件发生时提醒用户的方法。
然后,用户可以使用这些警报来决定特定事件是否值得将应用程序带回前台。
您可以通过在调用CBCentralManager
类的connectconnectPeripheral:options:
方法以连接到远程外设时包含以下外设连接选项之一来利用这些警报:
CBConnectPeripheralOptionNotifyOnConnectionKey
-如果您希望系统在成功连接时应用程序暂停时显示给定外围设备的警报,请包含此键。CBConnectPeripheralOptionNotifyOnDisconnectionKey
-如果您希望系统在断开连接时应用程序暂停时显示给定外围设备的断开连接警报,请包含此键。CBConnectPeripheralOptionNotifyOnNotificationKey
-如果您希望系统在应用程序暂停时显示从给定外围设备收到的所有通知的警报,请包含此键。
有关外设连接选项的详细信息,请参阅*CBCentralManager类参考*中详述的Peripheral Connection Options
常量。
2、核心蓝牙后台执行模式
如果您的应用需要在后台运行以执行某些蓝牙相关任务,它必须在其Information属性列表(Info.plist
)文件中声明支持Core蓝牙后台执行模式。
当您的应用声明这一点时,系统会将其从挂起状态唤醒,以允许其处理蓝牙相关事件。
这种支持对于与定期传递数据的蓝牙低功耗设备(如心率监测器)交互的应用很重要。
应用程序可以声明两种核心蓝牙后台执行模式——一种用于实现中心角色的应用程序,另一种用于实现外围角色的应用程序。
如果您的应用程序实现了这两个角色,它可能会声明它支持两种后台执行模式。
核心蓝牙后台执行模式的声明方法是将UIBackgroundModes
键添加到Info.plist
文件中,并将该键的值设置为包含以下字符串之一的数组:
bluetooth-central
-该应用程序使用核心蓝牙框架与蓝牙低功耗外围设备进行通信。bluetooth-peripheral
-应用程序使用核心蓝牙框架共享数据。
**注意:**Xcode中的属性列表编辑器默认显示许多键的人类可读字符串,而不是实际的键名。
要显示出现在Info.plist
文件中的实际键名,请控制单击编辑器窗口中的任何键,并在上下文窗口中启用显示原始键/值项。
有关如何配置Info.plist
文件内容的信息,请参阅Xcode帮助。
2.1 以蓝牙为中心的后台执行模式
当实现中心角色的应用程序在其Info.plist
文件中包含bluetooth-central
值的UIBackgroundModes
键时,核心蓝牙框架允许您的应用程序在后台运行以执行某些与蓝牙相关的任务。
当您的应用程序在后台时,您仍然可以发现并连接到外围设备,并探索外围数据并与之交互。
此外,当调用任何CBCentralManagerDelegate
或CBPeripheralDelegate
委托方法时,系统会唤醒您的应用程序,允许您的应用程序处理重要的中心角色事件,例如何时建立或拆除连接、何时外围设备发送更新的特征值以及何时中央管理器的状态发生变化。
尽管您可以在应用程序处于后台时执行许多与蓝牙相关的任务,但请记住,在应用程序处于后台时扫描外围设备的操作与在应用程序处于前台时的操作不同。
特别是,当应用程序在后台时扫描设备时:
- 忽略
CBCentralManagerScanOptionAllowDuplicatesKey
扫描选项键,并将广告外设的多个发现合并为单个发现事件。 - 如果扫描外围设备的所有应用程序都在后台,则您的中央设备扫描广告包的间隔会增加。
因此,发现广告外围设备可能需要更长的时间。
这些更改有助于最大限度地减少无线电使用,并延长iOS设备的电池寿命。
2.2 bluetooth-peripheral后台执行模式
要在后台执行某些外围角色任务,您必须在应用的Info.plist
文件中包含具有bluetooth-peripheral
值的UIBackgroundModes
键。
当该键值对包含在应用的Info.plist
文件中时,系统会唤醒您的应用以处理读取、写入和订阅事件。
除了允许您的应用被唤醒以处理来自连接中心的读、写和订阅请求之外,Core蓝牙框架还允许您的应用在后台状态下发布广告。
也就是说,您应该知道,当您的应用在后台时发布广告的操作与您的应用在前台时的操作不同。
特别是,当您的应用在后台时发布广告时:
- 忽略
CBAdvertisementDataLocalNameKey
通告键,不通告外设的本地名称。 - 包含在
CBAdvertisementDataServiceUUIDsKey
通告键值中的所有服务UUID都被放置在一个特殊的“溢出”区域中;它们只能由显式扫描它们的iOS设备发现。 - 如果所有投放广告的应用程序都在后台,则您的外围设备发送广告包的频率可能会降低。
3、明智地使用后台执行模式
尽管声明您的应用程序支持一种或两种核心蓝牙后台执行模式对于满足特定用例可能是必要的,但您应该始终负责任地执行后台处理。
因为执行许多与蓝牙相关的任务需要积极使用iOS设备的板载无线电——反过来,无线电使用会对iOS设备的电池寿命产生不利影响——所以请尽量减少您在后台所做的工作量。
因任何蓝牙相关事件而唤醒的应用程序应尽快处理它们并返回,以便应用程序可以再次暂停。
任何声明支持任何一种Core蓝牙后台执行模式的应用程序都必须遵循一些基本准则:
- 应用程序应该是基于会话的,并提供一个界面,允许用户决定何时开始和停止蓝牙相关事件的传递。
- 被唤醒后,应用程序有大约10秒的时间来完成任务。
理想情况下,它应该尽快完成任务,并允许自己再次暂停。
在后台执行时间过长的应用程序可能会被系统限制或杀死。 - 应用程序不应利用被唤醒作为执行与系统唤醒应用程序的原因无关的无关任务的机会。
有关应用程序在后台状态下的行为方式的更一般信息,请参阅*iOS应用程序编程指南*中的成为一个负责任的后台应用程序。
4、在后台执行长期操作
一些应用程序可能需要使用Core蓝牙框架在后台执行长期操作。
举个例子,假设你正在为一个与门锁(配备蓝牙低功耗技术)通信的iOS设备开发一个家庭安全应用程序。
应用程序和锁交互,在用户离开家时自动锁门,并在用户返回时解锁门——所有这些都是在应用程序处于后台的时候。
当用户离开家时,iOS设备最终可能会超出锁的范围,导致与锁的连接丢失。
此时,应用程序可以简单地调用CBCentralManager
类的connectPeripheral:options:
方法,并且由于连接请求不会超时,iOS设备将在用户回家时重新连接。
现在想象一下,用户离开家几天。
如果应用程序在用户离开时被系统终止,当用户回家时,应用程序将无法重新连接到锁,用户可能无法解锁门。
对于像这样的应用程序,能够继续使用Core蓝牙执行长期操作至关重要,例如监控活动和待处理的连接。
4.1 国家保护和恢复
由于Core蓝牙内置了状态保存和恢复功能,您的应用可以选择加入此功能,要求系统保存应用的中央和外围管理器的状态,并代表它们继续执行某些与蓝牙相关的任务,即使您的应用不再运行。
当其中一个任务完成时,系统会将您的应用重新启动到后台,并让您的应用有机会恢复其状态并适当地处理事件。
对于上述家庭安全应用,系统将监控连接请求,并重新启动应用以处理centralManager:didConnectPeripheral:
当用户返回家中并且连接请求完成时委托回调。
核心蓝牙支持实现中心角色、外围角色或两者兼而有之的应用的状态保存和恢复。
当您的应用实现中心角色并添加了对状态保存和恢复的支持时,系统会在系统即将终止您的应用以释放内存时保存您的中心管理器对象的状态(如果您的应用有多个中心管理器,您可以选择希望系统跟踪哪些)。
特别是,对于给定的CBCentralManager
对象,系统会跟踪:
- 中央管理器正在扫描的服务(以及扫描开始时指定的任何扫描选项)
- 中央管理器试图连接或已经连接的外围设备
- 中央管理者所认同的特征
实现外围角色的应用程序同样可以利用状态保存和恢复。
对于CBPeripheralManager
对象,系统会跟踪:
- 外设经理宣传的数据
- 外设管理器发布到设备数据库的服务和特征
- 符合你的特征值的中心
当系统将应用重新启动到后台时(例如,因为发现了应用正在扫描的外设),您可以重新实例化应用的中央和外设管理器并恢复它们的状态。
以下部分详细介绍如何利用应用中的状态保存和恢复。
4.2 增加对国家保护和恢复的支持
Core蓝牙中的状态保存和恢复是一项选择加入功能,需要您的应用程序提供帮助才能工作。
您可以按照以下过程在您的应用程序中添加对此功能的支持:
- (必需)当您分配和初始化中央或外围管理器对象时,选择进入状态保存和恢复。
此步骤在选择进入状态保存和恢复中进行了描述。 - (必需)系统重新启动应用程序后,重新实例化任何中央或外围管理器对象。
此步骤在重新实例化您的中央和外围管理器中描述。 - (必填)实施适当的恢复委托方法。
此步骤在实施适当的恢复委托方法中描述。 - (可选)更新中央和外围管理器的初始化过程。
此步骤在更新您的初始化过程中描述。
选择加入国家保护和恢复
要选择加入状态保存和恢复功能,只需在分配和初始化中央或外围管理器时提供唯一的恢复标识符。
恢复标识符是一个字符串,用于标识核心蓝牙和您的应用程序的中央或外围管理器。
字符串的值仅对您的代码重要,但此字符串的存在告诉核心蓝牙它需要保留标记对象的状态。
核心蓝牙仅保留具有恢复标识符的对象的状态。
例如,要在仅使用CBCentralManager
对象的一个实例来实现中心角色的应用中选择状态保存和恢复,请指定CBCentralManagerOptionRestoreIdentifierKey
初始化选项,并在分配和初始化中心管理器时为其提供恢复标识符。
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil
options:@{ CBCentralManagerOptionRestoreIdentifierKey: @"myCentralManagerIdentifier" }];
尽管上面的示例没有演示这一点,但您可以选择在以类似方式使用外围管理器对象的应用程序中进行状态保存和恢复:指定CBPeripheralManagerOptionRestoreIdentifierKey
初始化选项,并在分配和初始化每个外围管理器对象时提供恢复标识符。
注意: 因为应用程序可以有多个CBCentralManager
和CBPeripheralManager
对象的实例,所以请确保每个恢复标识符都是唯一的,以便系统可以正确区分一个中央(或外围)管理器对象和另一个。
重新实例化您的中央和外围管理器
当您的应用程序被系统重新启动到后台时,您需要做的第一件事是使用与首次创建时相同的恢复标识符重新实例化适当的中央和外围管理器。
如果您的应用程序只使用一个中央或外围管理器,并且该管理器在您的应用程序的生命周期内存在,则此步骤无需再做任何事情。
如果您的应用使用多个中央或外围管理器,或者如果它使用的管理器在您的应用生命周期内不存在,您的应用需要知道在系统重新启动时要重新实例化哪些管理器。
您可以访问系统在终止应用时为您的应用保留的管理器对象的所有恢复标识符的列表,方法是在实现应用委托的application:didFinishLaunchingWithOptions:
方法时使用适当的启动选项键(UIApplicationLaunchOptionsBluetoothCentralsKey
或UIApplicationLaunchOptionsBluetoothPeripheralsKey
)。
例如,当系统重新启动您的应用程序时,您可以检索系统为您的应用程序保留的中央管理器对象的所有恢复标识符,如下所示:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *centralManagerIdentifiers =
launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
...
在您有了恢复标识符的列表后,只需循环遍历它并重新实例化相应的中央管理器对象。
注意: 当您的应用程序重新启动时,系统仅为执行某些蓝牙相关任务的中央和外围管理器提供恢复标识符(当应用程序不再运行时)。这些启动选项键在中有更详细的描述 : UIApplicationDelegate Protocol Reference.
实施适当的恢复委托方法
在应用中重新实例化适当的中央和外围管理器后,通过将它们的状态与蓝牙系统的状态同步来恢复它们。
要使您的应用跟上系统代表它所做的事情(当它没有运行时),您必须实现适当的恢复委托方法。
对于中央管理器,实现centralManager:willRestoreState:
委托方法;对于外围管理器,实现peripheralManager:willRestoreState:
委托方法。
重要提示: 对于选择加入核心蓝牙的状态保存和恢复功能的应用,这些是第一个方法(centralManager:willRestoreState:
和peripheralManager:willRestoreState:
),当您的应用重新启动到后台以完成一些与蓝牙相关的任务时调用。
对于不选择加入状态保存(或者如果启动时没有任何恢复)的应用,首先调用centralManagerDidUpdateState:
和peripheralManagerDidUpdateState:
委托方法。
在上述两个委托方法中,最后一个参数是字典,其中包含有关终止应用时保留的管理器的信息。
有关可用字典键的列表,请参阅*CBCentralManagerDelegate协议参考中的Central Manager State Restoration Options
常量和CBPeripheralManagerDelegate协议参考*中的Peripheral_Manager_State_Restoration_Options
常量。
要恢复CBCentralManager
对象的状态,请使用centralManager:willRestoreState:
委托方法中提供的字典键。
例如,如果您的中央管理器对象在您的应用终止时有任何活动或待处理的连接,系统将代表您的应用继续监视它们。
如下所示,您可以使用CBCentralManagerRestoredStatePeripheralsKey
字典键获取中央管理器连接到或尝试连接到的所有外围设备(由CBPeripheral
对象表示)的列表:
- (void)centralManager:(CBCentralManager *)central
willRestoreState:(NSDictionary *)state {
NSArray *peripherals =
state[CBCentralManagerRestoredStatePeripheralsKey];
...
如何处理上述示例中的已恢复外围设备列表取决于用例。
例如,如果您的应用保留了中央管理器发现的外围设备列表,您可能希望将已恢复的外围设备添加到该列表中以保留对它们的引用。
如在您发现后连接到外围设备中所述,请务必设置外围设备的委托以确保它接收到适当的回调。
您可以通过类似的方式使用字典的键来恢复CBPeripheralManager
对象的状态,该字典提供在外围peripheralManager:willRestoreState:
委托方法中。
更新您的初始化过程
在您实现了前面三个必需的步骤之后,您可能想看看更新中央和外围管理器的初始化过程。
虽然这是一个可选步骤,但它对于确保应用程序中的事情顺利运行非常重要。
例如,您的应用程序可能在探索连接的外围设备的数据时被终止了。
当您的应用程序使用此外围设备恢复时,它不会知道它在终止时的发现过程中走了多远。
您需要确保从发现过程中中断的地方开始。
例如,在centralManagerDidUpdateState:
委托方法中初始化应用时,您可以确定是否成功发现了已恢复外围设备的特定服务(在应用终止之前),如下所示:
NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj, NSUInteger index, BOOL *stop) {
return [obj.UUID isEqual:myServiceUUIDString];
}];
if (serviceUUIDIndex == NSNotFound) {
[peripheral discoverServices:@[myServiceUUIDString]];
...
如上例所示,如果系统在发现服务之前终止了您的应用,此时可以通过调用discoverServices:
开始探索恢复的外围设备的数据。
如果您的应用成功发现服务,您可以检查是否发现了适当的特征(以及您是否已经订阅了它们)。
通过以这种方式更新初始化过程,您将确保在正确的时间调用正确的方法。
六、与远程外围设备交互的最佳实践
核心蓝牙框架使许多中心端事务对您的应用程序透明。
也就是说,您的应用程序可以控制并负责实现中心角色的大部分方面,例如设备发现和连接,以及探索和与远程外围设备的数据交互。
本章提供了以负责任的方式利用这种控制级别的指南和最佳实践,尤其是在为iOS设备开发应用程序时。
1、注意无线电使用和功耗
在开发与蓝牙低功耗设备交互的应用程序时,请记住蓝牙低功耗通信共享您设备的无线电以通过空中传输信号。
由于其他形式的无线通信可能需要使用您设备的无线电——例如,无线网络、经典蓝牙,甚至其他使用蓝牙低功耗的应用程序——开发您的应用程序,以尽量减少它对无线电的使用。
在为iOS设备开发应用程序时,尽量减少无线电使用尤其重要,因为无线电使用对iOS设备的电池寿命有不利影响。
以下指南将帮助您成为设备无线电的好公民。
因此,您的应用程序将表现更好,设备的电池将持续更长时间。
1.1 仅在需要时扫描设备
当您调用CBCentralManager
类的scanForPeripheralsWithServices:options:
方法来发现作为广告服务的远程外围设备时,您的中央设备使用其无线电来监听广告设备,直到您明确告诉它停止。
除非您需要发现更多设备,否则请在找到要连接的设备后停止扫描其他设备。
使用CBCentralManager
类的stopScan
方法停止扫描其他设备,如发现后连接到外围设备中所示。
1.2 仅在必要时指定CBCentralManagerScanOptionAllowDuplicatesKey选项
远程外围设备可能每秒发送多个广告数据包,以向监听中心宣布它们的存在。
当您使用scanForPeripheralsWithServices:options:
方法扫描设备时,该方法的默认行为是将广告外围设备的多个发现合并到一个发现事件中——也就是说,中央管理器为它发现的每个新外围设备调用centralManager:didDiscoverPeripheral:advertisementData:RSSI:
其委托对象的方法,无论它收到多少广告数据包。
当已经发现的外围设备的广告数据发生变化时,中央管理器也会调用此委托方法。
如果要更改默认行为,可以在调用scanForPeripheralsWithServices:options:
方法时将CBCentralManagerScanOptionAllowDuplicatesKey
常量指定为扫描选项。
当您这样做时,每次中央从外设接收到广告包时都会生成一个发现事件。
关闭默认行为对于某些用例可能很有用,例如根据外设的接近度(使用外设接收到的信号强度指示器(RSSI)值)启动与外设的连接。
也就是说,请记住,指定此扫描选项可能会对电池寿命和应用程序性能产生不利影响。
因此,仅在需要履行特定用例时才指定此扫描选项。
1.3 明智地探索外设数据
当您开发应用程序以满足特定用例时,外围设备可能具有比您感兴趣的更多的服务和特征。
发现外围设备的所有服务和相关特征会对电池寿命和应用程序的性能产生负面影响。
因此,您应该只寻找和发现您的应用程序需要的服务和相关特征。
例如,假设您连接到一个有许多可用服务的外围设备,但您的应用程序只需要访问其中的两个。
您可以通过将它们的服务UUID数组(由CBUUID
对象表示)传递给CBPeripheral
类的discoverServices:
方法来查找和发现这两个服务,如下所示:
[peripheral discoverServices:@[firstServiceUUID, secondServiceUUID]];
在您发现您感兴趣的两个服务后,您可以类似地只查找和发现您感兴趣的这些服务的特征。
同样,只需将标识您想要发现的特征的UUID数组(对于每个服务)传递给CBPeripheral
类的discoverCharacteristics:forService:
方法。
1.4 订阅经常变化的特性值
如检索特性的值中所述,有两种方法可以检索特性的值:
- 您可以通过每次需要特性值时调用
readValueForCharacteristic:
方法来显式轮询特性值。 - 您可以通过调用一次
setNotifyValue:forCharacteristic:
方法来订阅特性的值,以便在值更改时接收来自外围设备的通知。
最佳实践是尽可能订阅特性值,尤其是对于经常更改的特性值。
有关如何订阅特性值的示例,请参阅订阅到特性值。
1.5 当您拥有所需的所有数据时,断开与设备的连接
当不再需要连接时,您可以通过断开与外围设备的连接来帮助减少应用的无线电使用量。
在以下两种情况下,您都应该断开与外围设备的连接:
- 您订阅的所有特征值都已停止发送通知。
(您可以通过访问特征的isNotifying
属性来确定特征的值是否正在通知。) - 您可以从外围设备获得所需的所有数据。
在这两种情况下,请取消您可能拥有的任何订阅,然后断开与外设的连接。
您可以通过调用setNotifyValue:forCharacteristic:
方法,将第一个参数设置为NO
来取消与外设设备的任何订阅。
您可以通过调用CBCentralManager
类的cancelPeripheralConnection:
方法来取消与外设设备的连接,如下所示:
[myCentralManager cancelPeripheralConnection:peripheral];
注意: cancelPeripheralConnection:
方法是非阻塞的,任何CBPeripheral
类命令仍在等待您尝试断开连接的外设执行,可能会或可能不会完成执行。
因为其他应用程序可能仍与外设有连接,取消本地连接并不能保证底层物理链接立即断开。
但是,从您的应用程序的角度来看,外设被认为是断开的,中央管理器对象调用其委托对象的centralManager:didDisconnectPeripheral:error:
方法。
2、重新连接到外围设备
使用Core蓝牙框架,您可以通过三种方式重新连接到外设。
你可以:
- 使用
retrievePeripheralsWithIdentifiers:
方法检索已知外设列表——您过去发现或连接到的外设。
如果要查找的外设在列表中,请尝试连接到它。
此重新连接选项在检索已知外设列表中描述。 - 使用
retrieveConnectedPeripheralsWithServices:
方法检索当前连接到系统的外围设备列表。
如果要查找的外围设备在列表中,请将其本地连接到应用。
此重新连接选项在检索已连接外围设备列表中描述。 - 使用
scanForPeripheralsWithServices:options:
方法扫描并发现外设。
如果找到,请连接到它。
这些步骤在发现正在做广告的外设设备和发现后连接到外设设备中进行了描述。
根据用例,您可能不希望每次重新连接时都扫描并发现相同的外设。
相反,您可能希望先尝试使用其他选项重新连接。
如图5-1所示,一个可能的重新连接工作流程可能是按照上面列出的顺序尝试这些选项中的每一个。
图5-1示例重新连接工作流程
注意:您决定尝试的重新连接选项的数量以及您尝试的顺序可能因应用尝试实现的用例而异。
例如,您可能决定根本不使用第一个连接选项,或者您可能决定并行尝试前两个选项。
2.1 检索已知外设列表
第一次发现外设时,系统会生成一个标识符(UUID,由NSUUID
对象表示)来标识外设。
然后,您可以存储此标识符(例如,使用NSUserDefaults
类的资源),然后使用它来尝试使用CBCentralManager
类的retrievePeripheralsWithIdentifiers:
方法重新连接到外设。
下面描述了使用此方法重新连接到先前连接的外设的一种方法。
当你的应用启动时,调用retrievePeripheralsWithIdentifiers:
方法,传入一个数组,其中包含你之前发现并连接到的外围设备的标识符(以及你保存的标识符),如下所示:
knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers];
中央管理器会尝试将您提供的标识符与先前发现的外设的标识符进行匹配,并将结果作为CBPeripheral
对象数组返回。
如果未找到匹配项,则该数组为空,您应该尝试其他两个重新连接选项之一。
如果该数组不为空,则让用户选择(在UI中)尝试重新连接到哪个外设。
当用户选择一个外设时,尝试通过调用CBCentralManager
类的connectconnectPeripheral:options:
方法来连接到它。
如果外设仍然可以连接,中央管理器调用其委托对象的centralManager:didConnectPeripheral:
方法,外设成功重新连接。
**注意:**由于几个原因,外围设备可能无法连接。
例如,该设备可能不在中心附近。
此外,一些蓝牙低功耗设备使用周期性变化的随机设备地址。
因此,即使设备在附近,设备的地址也可能自系统上次发现以来发生了变化,在这种情况下,您尝试连接的CBPeripheral
对象与实际的外围设备不对应。
如果您无法重新连接到外围设备,因为它的地址已更改,您必须使用scanForPeripheralsWithServices:options:
方法重新发现它。
有关随机设备地址的更多信息,请参阅蓝牙4.0规范,第3卷,C部分,第10.8节和Apple产品的蓝牙附件设计指南。
2.2 检索连接的外设列表
重新连接到外设的另一种方法是检查您要查找的外设是否已经连接到系统(例如,通过另一个应用程序)。
您可以通过调用CBCentralManager
类的retrieveConnectedPeripheralsWithServices:
方法来做到这一点,该类返回表示当前连接到系统的外设设备的CBPeripheral
对象数组。
因为当前连接到系统的外设可能不止一个,所以您可以传入CBUUID
对象数组(这些对象表示服务UUID)来仅检索当前连接到系统的外设,并且包含由您指定的UUID标识的任何服务。
如果当前没有连接到系统的外设,则该数组为空,您应该尝试其他两个重新连接选项之一。
如果该数组不为空,则让用户(在UI中)选择尝试重新连接到哪一个。
假设用户找到并选择了所需的外设,则通过调用CBCentralManager
类的connectPeripheral:options:
方法将其本地连接到您的应用。
(即使设备已经连接到系统,您仍然必须在本地将其连接到您的应用才能开始探索并与之交互。)当本地连接建立时,中央管理器调用其委托对象的centralManager:didConnectPeripheral:
方法,外围设备成功重新连接。
七、将本地设备设置为外围设备的最佳实践
与许多中心端事务一样,核心蓝牙框架让您可以控制外围角色的大多数方面。
本章提供了以负责任的方式利用这种控制级别的指南和最佳实践。
1、广告注意事项
广告外围数据是设置本地设备以实现外围角色的重要组成部分。
以下部分以适当的方式帮助您执行此操作。
尊重广告数据的限制
您可以通过将广告数据字典传递给CBPeripheralManager
类的startAdvertising:
方法来宣传外围设备的数据,如宣传您的服务中所述。
创建广告字典时,请记住您可以宣传的内容和数量是有限制的。
虽然广告包通常可以包含有关外围设备的各种信息,但您可以仅宣传您的设备的本地名称和您想要宣传的任何服务的UUID。
也就是说,当您创建广告字典时,您可以仅指定以下两个键:CBAdvertisementDataLocalNameKey
和CBAdvertisementDataServiceUUIDsKey
。
如果您指定任何其他键,您将收到错误。
广告数据时可以使用多少空间也有限制。
当您的应用程序位于前台时,它可以在初始广告数据中使用最多28字节的空间,用于两个支持的广告数据键的任意组合。
如果此空间用完,扫描响应中还有额外的10字节空间,只能用于本地名称。
任何不适合分配空间的服务UUID都会添加到特殊的“溢出”区域;它们只能由显式扫描它们的iOS设备发现。
当您的应用程序处于后台时,本地名称不会被广告,所有服务UUID都放置在溢出区域中。
注意:这些大小不包括每种新数据类型所需的2字节标头信息。
广告和响应数据的确切格式在蓝牙4.0规范第3卷C部分第11节中定义。
为了帮助您保持在这些空间限制内,请将您宣传的服务UUID限制为那些标识您的主要服务的UUID。
仅在需要时为数据做广告
由于广告外设数据使用您本地设备的无线电(以及因此您设备的电池),因此仅在您希望其他设备连接到您时才进行广告宣传。
一旦连接,这些设备就可以直接探索和交互外设的数据,而不需要任何广告包。
因此,为了最大限度地减少无线电使用,提高应用程序性能,并保护您设备的电池,当不再需要促进任何预期的蓝牙低功耗交易时停止广告。
要停止在您的本地外设上投放广告,只需调用CBPeripheralManager
类的stopAdvertising
方法,如下所示:
[myPeripheralManager stopAdvertising];
让用户决定何时发布广告
通常只有用户才能知道何时做广告。
例如,当你知道附近没有其他蓝牙低能耗设备时,让你的应用程序在你的设备上宣传服务是没有意义的。
由于您的应用程序通常不知道附近有哪些其他设备,请在应用程序的用户界面(UI)中为用户提供一种决定何时发布广告的方式。
2、配置您的特征
创建可变特性时,您可以设置其属性、值和权限。
这些设置决定了连接的中心如何访问特性的值并与之交互。
尽管您可以根据应用的需要决定以不同的方式配置特性的属性和权限,但当您需要执行以下两项任务时,以下部分提供了一些指导:
- 允许连接的中心订阅您的特征
- 保护敏感特征值不被未配对的中心访问
配置您的特征以支持通知
如订阅经常更改的特征值中所述,建议中心订阅经常更改的特征值(远程外设服务的特征值)。
如果可能,通过允许连接的中心订阅您的特征值来鼓励这种做法。
创建可变特性时,通过使用CBCharacteristicPropertyNotify
常量设置特性的属性,将其配置为支持订阅,如下所示:
myCharacteristic = [[CBMutableCharacteristic alloc]
initWithType:myCharacteristicUUID
properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify
value:nil permissions:CBAttributePermissionsReadable];
在此示例中,特性的值是可读的,并且可以由连接的中心订阅。
需要配对连接才能访问敏感数据
根据用例,您可能希望出售具有一个或多个特征的服务,其价值需要安全。
例如,假设您想出售社交媒体个人资料服务。
此服务可能具有其值代表成员个人资料信息的特征,例如名字、姓氏和电子邮件地址。
很可能,您希望只允许受信任的设备检索成员的电子邮件地址。
您可以通过设置适当的特征属性和权限来确保只有受信任的设备才能访问敏感特征值。
要继续上面的示例,要仅允许受信任的设备检索成员的电子邮件地址,请设置适当特征的属性和权限,如下所示:
emailCharacteristic = [[CBMutableCharacteristic alloc]
initWithType:emailCharacteristicUUID
properties:CBCharacteristicPropertyRead
| CBCharacteristicPropertyNotifyEncryptionRequired
value:nil permissions:CBAttributePermissionsReadEncryptionRequired];
在此示例中,该特性被配置为只允许受信任的设备读取或订阅其值。
当连接的远程中心尝试读取或订阅此特性的值时,核心蓝牙会尝试将您的本地外围设备与中心配对以创建安全连接。
例如,如果中央设备和外围设备是iOS设备,则两个设备都会收到一个警报,指示另一个设备想要配对。
中央设备上的警报包含一个代码,您必须在外围设备警报的文本字段中输入该代码才能完成配对过程。
配对过程完成后,外设将配对的中央视为可信设备,并允许中央访问其加密特征值。
2024-06-14(五)