在iOS设备中使用BLE时,无论是将其作为central端还是peripheral端,其在通信时都会使用设备自身的无线电来发送信号。考虑到其它形式的无线通信也需要使用无线电,因此开发程序时应该尽量少使用无线电。另外,这对于设备电池的寿命及程序的性能也有所帮助。以此为出发点,我们将介绍一些使用BLE时的最佳实践,希望有所帮助。
与Peripheral设备交互的最佳实践
Core Bluetooth框架让程序的大部分Central端交互变得透明。即程序能够控制且有责任实现大部分Central端的操作,如设备搜索及连接,解析并与远程peripheral数据交互。下面我们将介绍一些Central端的最佳实践。
留意无线电的使用及电量消耗
只有当需要时才扫描设备
当调用CBCentralManager类的scanForPeripheralsWithServices:options:方法来搜索正在广告服务的peripheral设备时,central设备使用无线电来监听广告的设备,直到我们显示停止它。除非需要搜索更多的设备,否则当发现想要连接的设备时就停止扫描操作。此时可以调用CBCentralManager实例的stopScan方法来处理。
只有当需要时才指定CBCentralManagerScanOptionAllowDuplicatesKey选项
远程peripheral设备可能每秒发送多个广告包来声明它们的存在。当我们使用scanForPeripheralsWithServices:options:方法扫描设备时,该方法的默认行为是将多个搜索到的广告peripheral事件归集为一个事件—即central管理器在只有在每次发现新的peripheral时都调用其代理对象的centralManager:didDiscoverPeripheral:advertisementData:RSSI:,而不管它收到多少个广告包。central管理器在已发现的peripheral改变广告的数据时也会调用这个代理方法。
如果想要改变默认行为,可以指定CBCentralManagerScanOptionAllowDuplicatesKey作为扫描选项。此时,central管理器会在每次收到peripheral端的广告包时都触发一个事件。在某些情况下关闭默认行为很有用处,但记住指定CBCentralManagerScanOptionAllowDuplicatesKey扫描选项不利于电池的寿命及程序性能。因此,只在需要的时候使用这个选项以完成特定的任务。
解析peripheral数据
一个peripheral设备可能有多个服务和特性,但在我们的应用中,可能只对其中一些感兴趣。搜索peripheral设备的所有服务和特性可能不利于电池的寿命及程序性能。因此,我们只去搜索那些与我们的的应用相关的服务和特性。
例如,假设我们正在连接一个有很多可用服务的peripheral设备,但是我们的程序只需要访问其中两个。我们可以只查找这两个服务,即在调用CBPeripheral对象的discoverServices:方法时传入感兴趣服务的UUID的数组即可。如下代码所示:
[peripheral discoverServices:@[firstServiceUUID, secondServiceUUID]];
在搜索到这两个感兴趣的服务后,我们可以用类似的方法去搜索我们感兴趣的服务中的特性。此时调用CBPeripheral实例的discoverCharacteristics:forService:方法并传入特性UUID的数组。
订阅经常改变的特性值
我们可以通过两种方式获取特性的值:
在我们每次需要值时调用readValueForCharacteristic:方法来显示的轮循特性的值
调用setNotifyValue:forCharacteristic:方法来订阅特性的值,这样当值改变时我们可以收到来自于peripheral的通知。
通常最好是去订阅特性的值,特别是特性值经常改变时。
当获取到所有需要的数据时断开到设备的连接
当连接不再需要时,我们可以断开连接,以减少无线电的使用。在下面两种情况下,我们应该断开连接:
所有订阅的特性值已经停止发送通知(我们可以访问特性的isNotifying属性来查看属性值是否正在被通知)
我们已以获取来来自peripheral设备的全部值。
两种情况下,取自我们有的所有订阅并断开连接。我们通过调用setNotifyValue:forCharacteristic:方法并设置第一个参数为NO来取消订阅。同时调用CBCentralManager实例的cancelPeripheralConnection:方法来断开连接。注意这个cancelPeripheralConnection:方法是非阻塞的,如果我们尝试断开连接的peripheral设备仍然挂起,则CBPeripheral实例的命令可能完成执行,也可能没有。因为其它程序可能也连接着那个peripheral设备。取消一个本地连接不能保证底层物理链接会立即断开。
重新链接Peripheral
使用Core Bluetooth框架,有三种方式来重新连接peripheral设备:
使用retrievePeripheralsWithIdentifiers:方法获取已知peripheral设备的列表,这些设备是我们已经搜索并连接过的设备。如果我们查找的peripheral在列表中,则尝试重新连接。
使用retrieveConnectedPeripheralsWithServices:方法获取当前连接到系统的peripheral设备的列表。如果我们查找的peripheral设备在列表中,则连接它。
使用scanForPeripheralsWithServices:options:方法扫描并搜索peripheral设备。如果找到,则连接它。
根据使用的场景,我们可能不希望每次重新连接设备时,都去扫描并搜索设备。相反,我们可能想首先使用其它方式来重新连接。如下图所示,一个可能的重新连接操作流是按照上面列出来的方式去重新连接:
获取已知peripheral设备的列表
我们第一次发现一个peripheral设备时,系统生成一个标识符(NSUUID对象)来标识peripheral设备。我们可以存储这些设备,后续我们可以使用CBCentralManager实例的retrievePeripheralsWithIdentifiers:方法来重新连接这个peripheral设备。
当我们启动程序时,调用retrievePeripheralsWithIdentifiers:方法,传递一个我们先前搜索并连接过的peripheral设备的标识符的数组,如下代码所示:
knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers];
central管理器尝试在这个列表中匹配我们提供的标识符,并返回一个CBPeripheral对象的数组。如果没找到,则返回的数组为空,那么我们需要尝试另外两种方法。如果返回的数组不为空,则让用户选择连接哪一个peripheral设备。当用户选择后,调用CBCentralManager实例的connectPeripheral:options:方法来尝试连接。如果peripheral设备仍然可以连接,则central管理器调用代理对象的centralManager:didConnectPeripheral:方法,且成功连接上peripheral设备。
获取已连接peripheral设备的列表
另一种重新连接peripheral设备的方法是查看我们正在查找的设备是否正由系统连接着(如被其它程序连接着)。我们可以调用CBCentralManager实例的retrieveConnectedPeripheralsWithServices:方法,它返回一个表示当前系统正在连接着的peripheral设备的CBPeripheral对象的数组。
因为可以有多于一个peripheral设备正在连接着系统,我们可以传递一个CBUUID对象的数组来获取只包含指定UUID所标识服务的设备。如果当前系统没有连接任何peripheral设备,则返回数组为空,我们应该尝试其它两种方法。如果返回数组不为空,则让用户选择连接哪个设备。
假设用户找到并选择了需要的peripheral设备,则调用CBCentralManager实例的connectPeripheral:options:方法来连接它(即使系统已经连接了它,我们的程序仍然需要连接它以开始解析并交互数据)。当连接建立后,central管理器调用代理对象的centralManager:didConnectPeripheral:方法,然后成功连接peripheral设备。
将本地设备设置为peripheral设备的最佳实践
广告注意事项
在设置本地设备作为peripheral端时,广告peripheral数据是非常重要的一部分。我们下面将介绍一下如何以适当的方式来实现这一功能。
我们广告peripheral数据时,是将其放在一个字典中传递给CBPeripheralManager对象的startAdvertising:方法中。当创建一个广告字典时,需要知道我们可以广告什么及能广告多少数据。
虽然广告数据包通常可以放置关于peripheral设备的多种信息,但建议只放置设备的本地名及我们需要广告的服务的UUID。即,当创建广告字典时,可能只指定下面两个键:CBAdvertisementDataLocalNameKey和CBAdvertisementDataServiceUUIDsKey。如果指定其它键,则会收到一个错误。
同样,广告数据时也限定了可以使用多少空间。当程序在前台时,可广告的数据对于上述两个key值的任意组合来说,初始值不能超过28个字节。如果这个空间用完了,在扫描响应时可以有额外的10个字节的空间,但这只能用于本地名。任何超出的数据都会被放到一个指定的“溢出”区域;它们只能被显示扫描它们的iOS设备发现。当程序在后台时,本地名不能被广告,且所有的服务UUID都被放在溢出区域。
为了符合这此限制条件,我们需要将广告的服务UUID限制在主要服务的标识上。
另外,因为广告peripheral数据使用本地设备的无线电,所以只在需要其它设备连接的时候广告数据。一旦连接后,这些设备可以直接解析并交互数据,而不需要任何广告包。因此,为了减少无线电的使用、提高程序的性能及节省电量,当不再需要任何试图进行BLE交易时可以停止广告。为了停止本地peripheral,可以调用CBPeripheralManager对象的stopAdvertising方法,如下所示:
[myPeripheralManager stopAdvertising];
通常,只有用户才知道什么时候广告数据。例如,当我们知道没有任何BLE设备在附近时,在我们的设备上广告服务没有任何意义。因为我们的程序通常不知道是否有其它设备在附近,所以提供一个界面让用户来决定什么时候广告数据。
配置特性
当创建一个可变的特性时,我们设置它的属性、值和权限。这些设置决定了如何连接central访问及交互特性值。虽然我们可能基于程序的需求来配置特性的属性和权限,但当执行下面两种任务时,我们还是有章可循的
允许连接的central订阅属性
保护敏感特性值,不让其被未配对的central访问
对于这两种情况,首先我们配置特性以支持通知。通常建议central去订阅那些经常改变的特性值。当我们创建一个可变特性时,可以通过使用CBCharacteristicPropertyNotify常量来设置特性属性以支持订阅,如下所示:
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
在这个例子中,特性值是可读的,且可以被连接的central端订阅。
其它,要求配对的连接才能访问敏感数据。根据场景的不同,我们可能想要提供一个服务,这个服务有一个或多个需要加密值的特性。例如,假设我们想要提供一个社交媒体配置文件服务。这个服务有一些特性,它们的值表示成员的配置信息,如姓名、电子邮件地址。更可能的是,我们只允许受信任的设备来获取成员的电子邮件地址。
我们可以设置合适的特性属性及权限来确保只有受信任的设备可以访问敏感的特性值。继续上面的例子,为了只允许受信任的设备来获取成员的邮箱地址,可以如下设置合适的特性属性与权限:
emailCharacteristic = [[CBMutableCharacteristic alloc] initWithType:emailCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotifyEncryptionRequired value:nil permissions:CBAttributePermissionsReadEncryptionRequired];
在这个例子中,特性配置为只有受信任的设备才可以读取或订阅它的值。当一个连接的central尝试读取或订阅特性值时,Core Bluetooth尝试配对本地peripheral和central端来创建安全连接。
例如,如果central和peripheral都是iOS设备,两端都接收一个提示显示对方想要配对。central设备上的提示包含包含一个确认码,这个确认码必须在peripheral设备提示框的输入域中输入,来完成配对操作。
在配对成功后,peripheral认为配对的central是一个受信任的设备且允许central访问它的加密特性值。
小结
在使用BLE时,基于以下几点,程序开发过程中我们合理地使用蓝牙
程序性能
电池电量消耗
与其它通信方法争抢无线电资源
通常我们只在需要时才使用BLE,尽量减少设备扫描搜索操作。
参考
Core Bluetooth Programming Guide