核心蓝牙框架使许多中央端事务对我们的应用透明。也就是说,我们的应用可以控制并负责实现中心角色的大多数方面,例如设备发现和连接,以及浏览远程外围设备的数据并与之交互。本章提供了以负责任的方式利用此控制级别的指南和最佳做法,尤其是在为 iOS 设备开发应用时。
在开发与低功耗蓝牙设备交互的应用时,请记住,低功耗蓝牙通信共享设备的无线电以通过空中传输信号。由于其他形式的无线通信可能需要使用设备的无线电(例如 Wi-Fi、经典蓝牙,甚至其他使用低功耗蓝牙的应用),因此开发我们的应用以尽量减少使用无线电的次数。
在为 iOS 设备开发应用程序时,尽量减少无线电使用尤为重要,因为无线电使用会对 iOS 设备的电池寿命产生不利影响。以下准则将帮助我们成为设备无线电的好公民。因此,我们的应用将表现得更好,并且设备的电池将持续更长时间。
当我们调用 scanForPeripheralsWithServices: options: CBCentralManager 类的方法以发现作为广告服务的远程外围设备时,我们的中央设备使用其无线电侦听广告设备,直到我们明确告诉它停止。
除非需要发现更多设备,否则请在找到要连接的设备后停止扫描其他设备。使用 CBCentralManager 类的 stopScan 方法停止扫描其他设备,如发现外围设备后连接到外围设备中所示。
远程外围设备可能每秒发送多个广告数据包,以向侦听中心宣布其存在。使用 scanForPeripheralsWithServices: options: 方法扫描设备时,该方法的默认行为是将广告外设的多个发现合并为单个发现事件,也就是说,中央管理器为发现的每个新外设调用 centralManager: didDiscoverPeripheral: advertisementData: RSSI: 其委托对象的方法,无论它接收多少个广告数据包。当已发现的外围设备的广告数据发生更改时,中央管理器也会调用此委托方法。
如果要更改默认行为,可以在调用 scanForPeripheralsWithServices: options: 方法时将 CBCentralManagerScanOptionAllowDuplicatesKey 常量指定为扫描选项。执行此操作时,每次中心从外围设备接收广告数据包时,都会生成发现事件。关闭默认行为对于某些用例非常有用,例如根据外设的接近程度启动与外设的连接(使用外设接收信号强度指示器 (RSSI) 值)。也就是说,请记住,指定此扫描选项可能会对电池寿命和应用程序性能产生不利影响。因此,仅当需要满足特定用例时,才指定此扫描选项。
外围设备可能具有比我们在开发应用程序以满足特定用例时可能感兴趣的更多的服务和特征。发现外围设备的所有服务和相关特征可能会对电池寿命和应用性能产生负面影响。因此,应仅查找和发现应用所需的服务和相关特征。
例如,假设我们连接到具有许多可用服务的外围设备,但我们的应用只需要访问其中两个服务。我们只能查找和发现这两个服务,方法是将它们的服务 UUID(由 CBUUID 对象表示)的数组传递给 CBPeripheral 类的 discoverServices: 方法,如下所示:
[peripheral discoverServices: @[firstServiceUUID, secondServiceUUID]];
在我们发现我们感兴趣的两项服务后,我们可以同样地查找并仅发现我们感兴趣的这些服务的特征。同样,只需将标识要发现的特征(对于每个服务)的 UUID 数组传递给 CBPeripheral 类的 discoverFeatures: forService: 方法。
如检索特征的值中所述,有两种方法可以检索特征的值:
我们可以通过在每次需要该值时调用 readValueForCharacteristic: 方法显式轮询特征的值。
我们可以通过调用 setNotifyValue: forCharacteristic: 方法订阅特征的值,以便在值更改时从外围设备接收通知。
最佳做法是尽可能订阅特征的值,尤其是对于经常更改的特征值。有关如何订阅特征值的示例,请参阅订阅特征的值。
当不再需要连接时,我们可以通过断开与外围设备的连接来帮助减少应用的无线电使用量。在以下两种情况下,应断开与外围设备的连接:
我们已订阅的所有特征值都已停止发送通知。(我们可以通过访问特征的 isNotifying 属性来确定特征的值是否正在通知。
我们可以从外围设备获得所需的所有数据。
在这两种情况下,请取消我们可能拥有的任何订阅,然后断开与外围设备的连接。我们可以通过调用 setNotifyValue: forCharacteristic: 方法,将第一个参数设置为 NO,取消对特征值的任何订阅。我们可以通过调用 CBCentralManager 类的 cancelPeripheralConnection: 方法来取消与外围设备的连接,如下所示:
[myCentralManager cancelPeripheralConnection: peripheral];
注意: 取消外围设备连接: 方法是非阻塞的,任何仍待处理到我们尝试断开连接的外围设备的CBPeripheral类命令可能会也可能不会完成执行。由于其他应用可能仍与外围设备有连接,因此取消本地连接并不能保证立即断开基础物理链路的连接。但是,从应用的角度来看,外围设备被视为断开连接,中央管理器对象调用其委托对象的 centralManager: didDisconnectPeripheral: error: 方法。
使用核心蓝牙框架,我们可以通过三种方式重新连接到外围设备。我们可以:
使用检索外围设备(过去发现或连接到的外围设备)的列表,方法是检索外围设备与标识符: 方法。如果我们要查找的外围设备在列表中,请尝试连接到它。检索已知外围设备列表中介绍了此重新连接选项。
检索当前连接到系统的外围设备的列表,使用 retrieveConnectedPeripheralsWithServices: 方法。如果要查找的外围设备在列表中,请将其本地连接到应用。检索连接的外围设备列表中介绍了此重新连接选项。
使用scanForPeripheralsWithServices: options: 方法扫描并发现外围设备。如果找到它,请连接到它。发现正在播发并在发现外围设备后连接到外围设备的外围设备中介绍了这些步骤。
根据用例的不同,我们可能不希望每次要重新连接到外设时都必须扫描并发现相同的外设。相反,我们可能希望先尝试使用其他选项重新连接。如图 5-1 所示,一种可能的重新连接工作流可能是按照上面列出的顺序尝试其中每个选项。
图 5-1 重新连接工作流示例
注意: 我们决定尝试的重新连接选项数量及其顺序可能会因应用尝试满足的用例而异。例如,我们可能决定根本不使用第一个连接选项,或者我们可以决定并行尝试前两个选项。
首次发现外设时,系统会生成一个标识符(由 NSUUID 对象表示的 UUID)来标识外设。然后,我们可以存储此标识符(例如,使用 NSUserDefaults 类的资源),稍后使用它尝试使用 CBCentralManager 类的 retrievePeripheralsWithIdentifiers: 方法重新连接到外围设备。下面介绍了使用此方法重新连接到之前连接的外围设备的一种方法。
当应用启动时,调用 retrievePeripheralsWithIdentifiers: 方法,传入一个数组,其中包含我们之前发现并连接到的外围设备的标识符(以及已保存的标识符),如下所示:
knownPeripherals =
[myCentralManager retrievePeripheralsWithIdentifiers: savedIdentifiers];
中央管理器尝试将我们提供的标识符与以前发现的外围设备的标识符进行匹配,并将结果作为 CBPeripheral 对象数组返回。如果未找到匹配项,则数组为空,我们应该尝试其他两个重新连接选项之一。如果数组不为空,则让用户(在 UI 中)选择要尝试重新连接到的外设。
当用户选择外围设备时,尝试通过调用 CBCentralManager 类的 connectPeripheral: options: 方法来连接到它。如果外围设备仍可连接,则中央管理器调用其委托对象的 centralManager: didConnectPeripheral: 方法,并成功重新连接外围设备。
注意: 由于一些原因,外围设备可能无法连接。例如,设备可能不在中央附近。此外,某些低功耗蓝牙设备使用定期更改的随机设备地址。因此,即使设备在附近,设备的地址也可能自上次被系统发现以来已更改,在这种情况下,我们尝试连接到的 CBPeripheral 对象与实际外围设备不对应。如果由于外围设备的地址已更改而无法重新连接到外围设备,则必须使用 scanForPeripheralsWithServices: options: 方法重新发现它。
重新连接到外围设备的另一种方法是检查我们要查找的外围设备是否已连接到系统(例如,通过另一个应用程序)。可以通过调用 CBCentralManager 类的 retrieveConnectedPeripheralsWithServices: 方法来实现此目的,该方法返回表示当前连接到系统的外围设备的 CBPeripheral 对象的数组。
由于当前可能有多个外围设备连接到系统,因此可以传入 CBUUID 对象数组(这些对象表示服务 UUID),以仅检索当前连接到系统的外围设备,并包含由我们指定的 UUID 标识的任何服务。如果当前没有外围设备连接到系统,则阵列为空,我们应该尝试其他两个重新连接选项之一。如果数组不为空,则让用户(在 UI 中)选择要尝试重新连接到哪个数组。
假设用户找到并选择了所需的外围设备,则通过调用 CBCentralManager 类的 connectPeripheral: options: 方法,将其本地连接到应用。 (即使设备已连接到系统,仍必须将其本地连接到应用才能开始浏览和与之交互。当建立本地连接时,中央管理器调用其委托对象的 centralManager: didConnectPeripheral: : 方法,外围设备重新连接成功。