其他文章
蓝牙一 有关代理协议详解 和 info.plist设置
第一部分:蓝牙概述
第二部分:central角色的实现
第三部分:peripheral角色的实现
第四部分:iOS蓝牙应用的后台处理
第五部分:与peripheral通信的最佳实践
第六部分:作为Peripheral端的最佳方式
一、蓝牙概述
1.1 central
和 peripheral
在蓝牙通讯中的关系
central
和peripheral
设备及他们在蓝牙通讯中的角色。
蓝牙通讯中的两个重要的角色,central
和peripheral
。相对于传统的client-server
架构,peripheral
代表着拥有数据的一方并且外界是需要这数据的。central
代表着使用数据方,从peripheral
那里获得数据并完成特定的事情。
centrals
发现并连接到正在广播的peripherals
peripheral
在广播包中广播他们的数据。广播包中应该包含一些有用的信息,比如peripheral
的名称和主要功能等。
central
可以扫描和监听任何他感兴趣的peripheral
设备。
1.2peripheral
如何组织数据
peripheral
可以包含一个或多个services
或提供它们连接信号强度的信息。service
是数据和辅助行为的集合。
service
本身由characteristics
和引用的services
组成。characteristic
提供服务的详细的信息。
1.3 central
检索并与peripheral
交互数据
建立连接之后,就可以发现所有的services
和characteristics
(广播中可能只包含部分services
)
central
可以通过services
的characteristic
来读或写数据。
1.4 centrals
,peripherals
和 peripheral
的数据如何表示
角色和数据在蓝牙框架中以一种简单直接的方式表示。
1.4.1 central
端的对象
当你使用central
来与peripheral
通讯,你执行的是central
端的操作。
在central
端,设备用CBCentralManager
对象代表。这个对象用来管理被发现的已连接的peripherals
。
peripheral
的数据用CBService
和CBCharacteristic
来表示。
当你与peripheral
交互时,你需要使用他的sevices
和characteristics
。
在蓝牙sdk
中service
用CBService
对象表示,characteristic
用CBCharacteristic
对象表示。
1.4.2 peripheral
端的对象
如果是安装OS X v10.9
或iOS6
,那你的mac
和ios
设备就可以作为peripheral
端设备,为其他设别提供数据。让你的设备实现peripheral
角色,执行peripheral
端的功能。
本地peripherals
和远端centrals
在peripheral
端,peripheral
设备用CBPeripheralManager
对象表示。这个对象用来管理发布services
和characteristics
,并发出广播。同时peripheral manager
可用来响应读和写请求。
本地peripheral
的数据用CBMutableService
和CBMutableCharacteristic
对象表示
当你创建并想设置peripheral
的数据。你需要处理peripheral manager
对象的services (CBMutableService实例)
,同样,characteristic
的实例也是CBMutableCharacteristic
对象。
二、central
角色的实现
2.1 central
角色的实现
central
角色需要完成的几件事情,如 :搜索,连接,与peripheral交互数据。
peripheral
角色同样需要完成几件事情,如:发布和广播服务,响应读,写,订阅请求
接下来,你将学习如何完成central
端的功能。
- 1,创建
central manager
对象 - 2,发现和连接正在广播的
peripheral
- 3,浏览
peripheral
的数据 - 4,发送读和写请求到
peripheral
设备 - 5,订阅
characteristic
的值
2.1.1 创建Central manager
对象
引入头文件#import
遵守协议方法
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
方法说明:
在这里self
设置成central
角色的代理。dispath queue
设置为nil
,意味着central
事件将交由main queue
处理。
创建central manager
时,会触发centralManagerDidUpdateState:
代理方法。你必须实现这个代理。
// 中心设备的蓝牙状态发生变化之后会调用此方法 [必须实现的方法]
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
// 中心设备状态枚举
typedef NS_ENUM(NSInteger, CBCentralManagerState) {
CBCentralManagerStateUnknown = CBManagerStateUnknown,// 蓝牙状态未知
CBCentralManagerStateResetting = CBManagerStateResetting,
CBCentralManagerStateUnsupported = CBManagerStateUnsupported, // 不支持蓝牙
CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized, // 蓝牙未授权 CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff, // 蓝牙关闭状态
CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn, // 蓝牙开启状态
} NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead");
2.1.2 搜索正在发送广播的peripheral
[myCentralManager scanForPeripheralsWithServices:nil options:nil];
方法说明:
注意:如果第一个参数设置成nil
,那么centralmanager
会返回所有被发现的peripherals
,在实际应用中,你应该给他赋值 CBUUID
对象数组。这样只有有广播这些uuid
服务的peripheral
才会被返回,一旦发现peripheral
,将触发centralManager:didDiscoverPeripheral:advertisementData:RSSI:
代理方法,如果你想连接这个peripheral
,请使用强引用变量引用它,这样系统就不会释放掉它。
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
NSLog(@"Discovered %@", peripheral.name);
self.discoveredPeripheral = peripheral;
...
}
如果你想连接多个设备,你可以使用NSArray
来管理他们。不管什么情况,一旦你找到所有你想要的设备,你应该停止扫描以便节省电量。
[myCentralManager stopScan];
2.1.3 连接peripheral
[myCentralManager connectPeripheral:peripheral options:nil];
建议使用下面这种连接外设方式
[self.manager connectPeripheral:peripheral
options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:CBConnectPeripheralOptionNotifyOnDisconnectionKey]];
}
options
:参数说明
CBConnectPeripheralOptionNotifyOnConnectionKey: 在应用挂起后,与指定的peripheral成功建立连接,则发出通知(在连接成功后,程序被挂起,给出系统提示。
)
CBConnectPeripheralOptionNotifyOnDisconnectionKey: 在应用挂起后,如果与指定的peripheral断开连接,则发出通知(在程序挂起,蓝牙连接断开时,给出系统提示。
)
CBConnectPeripheralOptionNotifyOnNotificationKey: 在应用挂起后,指定的peripheral有任何通知都进行提示,建议使用这个( 在程序挂起后,收到 peripheral 数据时,给出系统提示。 Core Bluetooth 后台模式
)
如果连接成功,则会触发centralManager:didConnectPeripheral:
代理方法,与之通讯之前,你需要为它设置代理
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
NSLog(@"Peripheral connected");
peripheral.delegate = self; // 设置代理
...
}
2.1.4 查询peripheral
的服务
建立连接之后,就可以查询数据,第一步就是查询有哪些services
。因为广播包有大小限制,在广播中可能没有显示全部的服务信息,这里你可以使用discoverServices:
来查询所有的services。
[peripheral discoverServices:nil];
注意:实际开发中,你不应该传值nil
,因为这样做会返回所有的services
,包括那些你不需要的services
,这样做会浪费时间和电量。所以你应该传递你需要的uuids
。
查找到所有的服务会触发peripheral:didDiscoverServices:
代理方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
for (CBService *service in peripheral.services) {
NSLog(@"Discovered service %@", service);
...
}
2.1.5 查找characteristics
发现service
之后,下一步就是查找characteristics
NSLog(@"Discovering characteristics for service %@", interestingService);
[peripheral discoverCharacteristics:nil forService:interestingService];
注意:实际开发中,不要传nil
,这样做会返回所有的characteristics
,包括那些你不感兴趣的characteristics
。这样做即浪费时间又损耗电量。所以你应该传你需要的uuids
的值
查找到characteristics
之后,会触发peripheral:didDiscoverCharacteristicsForService:error:
代理方法
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
for (CBCharacteristic *characteristic in service.characteristics) {
NSLog(@"Discovered characteristic %@", characteristic);
...
}
2.1.6 获取数据
获取到数据后peripheral
调用peripheral:didUpdateValueForCharacteristic:error:
代理方法
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
NSData *data = characteristic.value;
// parse the data as needed
...
}
一个characteristic
只包含一种信息数据。比如恒温service
下的温度characteristic
只包含当前温度值,你可以通过读取或订阅来获得这个值。
####### 2.1.6.1 读取characteristic
的数据
NSLog(@"Reading value for characteristic %@", interestingCharacteristic);
[peripheral readValueForCharacteristic:interestingCharacteristic];
注意:并不是所有的characteristic
都是可读的,你可以通过 characteristic
的proterties
属性是否包含CBCharacteristicPropertyRead
常量来判断是否可读。在读一个不可读的characteristic
的数据值,会在代理方法的error
参数中体现异常信息
####### 2.1.6.2订阅characteristic的值
虽然通过readValueForCharacteristic:
可以有效获取静态值,但如果值是动态改变的,则最好使用订阅的方法。当值变化时,自动获得通知。
设置订阅
[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];
订阅的时候peripheral
会调用peripheral:didUpdateNotificationStateForCharacteristic:error:
代理方法。如果订阅失败了,也可以通过这个方法查询失败的原因。
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error) {
NSLog(@"Error changing notification state: %@",[error localizedDescription]);
}
}
注意:并不是所有的characteristics
都可以订阅,可以通过检查characteristic
的properties
属性是否包含CBCharacteristicPropertyNotify
或CBCharacteristicPropertyIndicate
常量来判断是否可订阅。
2.1.7 写入数据
有时也是需要写数据的,比如恒温器,你需要设置目标温度。如果characteristic
是可写的,那么你就可以调用writeValue:forCharacteristic:type:
方法来写入数据。如下:
NSLog(@"Writing value for characteristic %@", interestingCharacteristic);
[peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic type:CBCharacteristicWriteWithResponse];
当你写数据时,你可以标明写类型。上例中,写类型是CBCharacteristicWriteWithResponse
, 这种情况下,不管有没有写成功,peripheral
都会通过代理通知你。你需要实现这个方法以便处理异常情况。
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if (error) {
NSLog(@"Error writing characteristic value: %@",[error localizedDescription]);
}
}
如果你设置的写类型是CBCharacteristicWriteWithoutResponse
, 那么写操作会以更有效的方式执行,但不保证写成功,并且不会有报告。peripheral
不会通知任何回调。
注意:characteristic
可能只支持特定的写类型,或不支持写操作。你可以通过检查properties
属性是否包含CBCharacteristicPropertyWriteWithoutResponse
或 CBCharacteristicPropertyWrite
来判断。
三、peripheral
角色的实现
3.1 peripheral
角色的实现
接下来,你将学习如何使用peripheral
。
- 1,创建
peripheral manager
对象 - 2,构建
services
和characteristics
- 3,发布
services
和characteristics
到数据库 - 4,广播你的服务
- 5,响应读和写请求
- 6,发送数据给
centrals
订阅者
3.1.1 创建外设管理器
myPeripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];
方法说明:创建的时候,peripheral manager
将调用代理对象的peripheralManagerDidUpdateState:
方法。
3.1.2 构建服务和特征值
services
和 characteristic
是树形结构组织的,服务和特征值使用uuid
标识。
CBUUID *heartRateServiceUUID = [CBUUID UUIDWithString: @"180D"];
- 1、为
services
和characteristics
创建你自己的UUID
CBUUID *myCustomServiceUUID = [CBUUID UUIDWithString:@"71DA3FD1-7E10-41C1-B16F-4430B506CDE7"];
- 2、构建
services
和characteritics
树
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable];
propeties
和permissions
的设置决定了这个characteristic
是否可读,是否可写,是否可订阅。上例中我们把它设置成可读。
注意:如果你设置value
的值,这个值将被缓存,并且properties
和permissions
将是只读的。因此,如果你希望value
是可写的,或value
值根据具体情况呈不同的值时,你必须把它设置成nil
。这样才能使它被动态赋值。才能响应peripheral manager
的请求。
补充:
- 1.针对
CBMutableCharacteristic
的properties
属性的说明:
CBCharacteristicPropertyBroadcast: 允许一个广播特性值,用于描述特性配置,不允许本地特性
CBCharacteristicPropertyRead: 允许读一个特性值
CBCharacteristicPropertyWriteWithoutResponse: 允许写一个特性值,没有反馈
CBCharacteristicPropertyWrite: 允许写一个特性值
CBCharacteristicPropertyNotify: 允许通知一个特性值,没有反馈
CBCharacteristicPropertyIndicate: 允许标识一个特性值
CBCharacteristicPropertyAuthenticatedSignedWrites: 允许签名一个可写的特性值
CBCharacteristicPropertyExtendedProperties: 如果设置后,附加特性属性为一个扩展的属性说明,不允许本地特性
CBCharacteristicPropertyNotifyEncryptionRequired: 如果设置后,仅允许信任的设备可以打开通知特性值
CBCharacteristicPropertyIndicateEncryptionRequired: 如果设置后,仅允许信任的设备可以打开标识特性值
- 2.针对
CBMutableCharacteristic
的permissions
权限的说明:
CBAttributePermissionsReadable // 读
CBAttributePermissionsWriteable // 写
CBAttributePermissionsReadEncryptionRequired //需要连接成功后,允许读(建议:安全数据这么做)
CBAttributePermissionsWriteEncryptionRequired //需要连接成功后,允许写(建议:安全数据这么做)
- 2.1 创建
service
myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];
说明:这里的第二个参数设置为YES
,标明这个服务值主要的。主要服务体现了主要的功能,并能够被其它服务引用。次要服务只用来描述其引用的服务的相关的信息。比如,心率监控的主要服务用来显示心率数据,这时次要服务可能就用来显示电池数据。
- 2.2 关联
characteristics
myService.characteristics = @[myCharacteristic];
- 3 发布服务和特征值
[myPeripheralManager addService:myService];
这里会触发代理消息
- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{
if (error){
NSLog(@"Error publishing service: %@", [error localizedDescription]);
}
}
如果有异常,请通过error
查询。
注意:服务和特征值一旦发布,不能更改。
- 4 广播你的服务
[myPeripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey :@[myFirstService.UUID, mySecondService.UUID] }];
例子中参数是dictionary
,不是array
。并且只有一个key
。在这里只支持两个key
,CBAdvertisementDataLocalNameKey
和 CBAdvertisementDataServiceUUIDsKey
。详情参见Advertisement Data Retrieval Keys in CBCentralManagerDelegate Protocol Reference。
当你开始广播你的服务,peripheral manager
就会通知代理peripheralManagerDidStartAdvertising:error:
。如果有异常将不会发出广播,并在代理中可查到异常的原因:
- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error {
if (error) {
NSLog(@"Error advertising: %@", [error localizedDescription]);
}
}
当你发出广播后,远程的centrals
将可发现并初始化来取得连接。
5 相应
centrals
的读写请求
在连接到远程的centrals
之后,你就可以开始接收读或写请求。5.1 读请求
当central
有读请求时,peripheral manager
调用peripheralManager:didReceiveReadRequest:
代理方法。并把请求信息通过CBATTRequest
传过来。
比如当你收到一个读请求时,CBATTRequest
对象的属性可以确保设备的数据库中的特征值能匹配central
所标记的那个特征值。代理的方法实现类似如下:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveReadRequest:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
...
}
如果特征值的UUID
匹配,下一步就是确保请求所读的数据不越界。
if (request.offset > myCharacteristic.value.length) {
[myPeripheralManager respondToRequest:request
withResult:CBATTErrorInvalidOffset];
return;
}
如果请求的偏移量没有越界,那么设置请求的值。
request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset,myCharacteristic.value.length - request.offset)];
设置好返回值(request.value
)之后,使用respondToRequest:withResult:
方法来返回数据,类似如下:
[myPeripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
每次收到的请求peripheralManager:didReceiveReadRequest:
都应该有相应的返回respondToRequest:withResult:
方法。
注意:假如特征值的UUID
不匹配,或是读请求由于某些原因无法完成。你也应该调用respondToRequest:withResult:
方法来返回失败结果。失败结果列表参见CBATTError Constants enumeration in Core Bluetooth Constants Reference。
- 5.2 写请求
写请求的处理也很简单。当central
发送请求要求写数据时,peripheral manager
调用peripheralManager:didReceiveWriteRequests:
代理方法,数据通过参数CBATTRequest
传递给你。
比如:当你收到一个写请求时,CBATTRequest
对象的属性可以确保设备的数据库中的特征值能匹配central
所标记的那个特征值。代理的方法实现类似如下:
- (void)peripheralManager:(CBPeripheralManager *)peripheral
didReceiveWriteRequests:(CBATTRequest *)request {
if ([request.characteristic.UUID isEqual:myCharacteristic.UUID]) {
...
}
如果特征值的UUID
匹配,下一步就是确保请求所读的数据不越界。
if (request.offset > myCharacteristic.value.length) {
[myPeripheralManager respondToRequest:request
withResult:CBATTErrorInvalidOffset];
return;
}
如果请求的偏移量没有越界,那么设置请求的值。
myCharacteristic.value = request.value;
这里同样需要注意请求的偏移量问题。
跟读请求相似,每次都要调用respondToRequest:withResult:
方法来回应消息。也就是说,虽然代理中入参是array
,可能包含多个CBATTRequest
,但是回应时是单个的CBATTRequest
对象。你必须传入array
中的第一个对象。如下:
[myPeripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];
注意:把多个请求看成一个对待,如果其中有一个请求无法实现,那么所有的请求就将无法实现。同时,调用respondToRequest:withResult:
方法回应,并提供失败的原因。
- 6 订阅
Characteristic Values
通常,centrals
可以订阅一个或多个特征值,这在Subscribing to a Characteristic’s Value.
中也有描述。这种情况下如果他们订阅的特征值的值有变化,你应该要能够给他们发消息。 - 6.1 调用代理方法
当一个central
订阅某个特征值,peripheral manager
将通知代理peripheralManager:central:didSubscribeToCharacteristic:
方法。
- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic {
NSLog(@"Central subscribed to characteristic %@", characteristic);
}
- 6.2 获取更新的值并发送给
central
NSData *updatedValue = // fetch the characteristic's new value
BOOL didSendValue = [myPeripheralManager updateValue:updatedValue forCharacteristic:characteristic onSubscribedCentrals:nil];
在你调用这个方法来发送数据给订阅者时,你可以在最后一个参数标明哪个central
,如上。如果你写nil
,所有连接着的有订阅的centrals
都将收到信息,当然有连接但没订阅的会被忽略。
updateValue:forCharacteristic:onSubscribedCentrals:
这个方法返回Boolean
值,指明数据是否成功发送。如果底层的队列正在传输数据,这个方法就会返回NO
。当传输队列重新变为空闲时,则会调用peripheral manager
的代理方法peripheralManagerIsReadyToUpdateSubscribers:
,这时你就可以利用这个代理重新发送数据,而不需要重新调用updateValue:forCharacteristic:onSubscribedCentrals:
方法。
注意:使用通知来发送一个数据包给central
订阅者。也就是说,当你需要给订阅者更新数据时,你应该在通知中发送整个数据,通过一次调用updateValue:forCharacteristic:onSubscribedCentrals:
方法来实现。局限于特征值数据大小的限制,并不是所有数据都能用通知来传递。这种情况下,应该由central
端通过调用CBPeripheral
的readValueForCharacteristic:
方法来获取整个数据。
四、iOS
蓝牙应用的后台处理
4.1 iOS
蓝牙应用的后台处理
对于iOS
应用,你必须要清楚它是在前台运行,还是在后台运行。因为资源有限,你要对这两种模式区别处理。
默认情况下,当应用进入后台或挂起时,蓝牙任务是不执行的。但是,你可以把应用声明为支持蓝牙后台执行模式,这样当有蓝牙相关事件发生时,你的应用就可以被唤醒来处理任务。即使你的应用不要求后台处理支持,当有重要的事件发生时,系统仍然可能跳出警告,要求处理。
即使你的应用支持一种或两种都支持后台执行模式,也不是就一定能永远执行。在某些情况下,系统可能终止你的应用以便为前台应用让出内存,这将导致当前活动和连接等信息丢失。自iOS7
之后,蓝牙库支持保存状态信息,并可在下次启动app
时还原状态信息。你可以通过这个特性来实现长连接。
4.1.1 只支持前台运行的应用
大部分的apps
,除非你要求后台运行,在进入后台后,应用会很快被挂起。在挂起状态下,应用无法处理蓝牙相关任务,无法接收蓝牙事件。直到重新回到前台。
在central
端,只支持前台运行的应用,在进入后台或被挂起时就无法扫描和发现peripheral
的广播包。如果是在peripheral
端,广播将停止,任何central
想访问characteristic
的值都将收到异常信息。
不同情况下,默认的行为可能会影响你的程序。比如,在你与peripheral
交互数据时,应用挂起(比如用户切到另一个应用)。这时连接可能会断开,你并不会收到通知,直到应用重新激活。
####### 4.1.1.1 利用peripheral
连接选项
只支持前台的蓝牙应用在挂起后发生的蓝牙事件会被系统排队,并在应用进入前台时把事件发给应用。当特定的central
事件发生时,蓝牙库可以提供一种方式来提示用户。用户可以根据这些提示来决定是否激活应用。
若想利用使用这些提示,你需要在调用connectPeripheral:options:
方法时传入如下参数。
CBConnectPeripheralOptionNotifyOnConnectionKey:
在应用挂起后,与指定的peripheral
成功建立连接,则发出通知
CBConnectPeripheralOptionNotifyOnDisconnectionKey:
在应用挂起后,如果与指定的peripheral
断开连接,则发出通知
CBConnectPeripheralOptionNotifyOnNotificationKey:
在应用挂起后,指定的peripheral
有任何通知都进行提示
####### 4.1.1.2 后台执行模式
如果你的应用在后台时也需要处理蓝牙事件,就必须在Info.plist中声明应用要支持蓝牙后台模式,这样,当有蓝牙事件发生时,系统会唤醒应用来处理。
有两种蓝牙后台模式,一种为central
角色,另一种为peripheral
角色。如果应用需要两种角色,则可以声明支持两种模式。声明方式:增加UIBackgroundModes
键,并增加包含下列字符串的array
值。
• bluetooth-central —The app communicates with Bluetooth low energy peripherals using the Core Bluetooth framework.
• bluetooth-peripheral —The app shares data using the Core Bluetooth framework
注意:Info.plist
中会显示为更加人性化的文本,不是直接显示实际的键值对。如要显示实际值,可右键,或control
点击,在弹出菜单中选择Open As -> Source Code
。
UIBackgroundModes
bluetooth-central
bluetooth-peripheral
或者(推荐使用这个操作)
对后台模式的说明:
- 支持
central
后台运行的模式
如果你的应用支持central
角色的后台模式,也就是Info.plist
中UIBackgroundModes
键的值中包含bluetooth-central
值。那么应用将可以在后台处理特定的蓝牙相关事件。即使在后台,你仍然可以发现和连接peripherals
,可以检索和读写数据。并且当有CBCentralManagerDelegate or CBPeripheralDelegate
代理事件发生时,系统会唤醒应用来处理。
需要注意的是,进入后台时,扫描的处理有些区别:
- 1,
CBCentralManagerScanOptionAllowDuplicatesKey
这个键会被忽略,多次发现同一peripheral
会被合并成一个发现事件。- 2,如果所有扫描中的应用都在后台,那么你应用的扫描间隙会延长。结果是,扫描到
peripheral
的时间可能会延长。这样做是为了减少辐射节省电量。
- 支持
peripheral
后台运行的模式
如果要支持peripheral
角色的后台模式,你需要在Info.plist
中的增加UIBackgroundModes
键并在值中包含bluetooth-peripheral
值。这样系统会唤醒应用来处理读写和订阅事件。
蓝牙框架(Core Bluetooth framework
)不仅允许你的应用被唤醒来处理读写和订阅请求,还允许你的应用在后台状态下发送广播。但你必须注意后台时广播与前台时广播是不同的。即便如此,你必须注意后台与前台时广播处理的区别。特别是当你的应用需要在后台发送广播。
- 1,
CBAdvertisementDataLocalNameKey
这个键会被忽略,并且peripheral
的local name
不会被广播- 2,
CBAdvertisementDataServiceUUIDsKey
的值中包含的所有service uuids
都会被放到“overflow”
区域;只有iOS
设备显示指明在搜索它时才会搜索到这些值。- 3,如果所有的处于广播状态的应用都在后台,广播频率将降低。
####### 4.1.1.3 明智使用后台运行模式
虽然为了完成某些事情,有必要把你的应用声明成支持后台运行模式,你也应该要能有效处理后台任务。因为执行蓝牙任务会使用无线电,从而耗费电池电量,所以尽量最小化后台任务。应用被蓝牙事件唤醒后应能尽快处理好任务,以便被重新挂起。
支持后台运行的任务要遵循几个原则
- 1,应用应该是基于会话的,并提供接口让用户决定何时开始或停止蓝牙事件。
- 2,应用被唤醒后,大约有
10秒钟
的时间来完成任务,所以应该尽快完成任务并重新挂起。若在后台花费太多时间,则将受到系统的遏制甚至被扼杀。- 3,应用不应该使用这种被唤醒的机会来执行与之无关的事情。
####### 4.1.1.4 后台长时间执行
一些应用需要长时间后台运行。举个例子,你可发一款家庭安全应用,iOS
设备与蓝牙门锁通讯。当用户离开家时自动锁门,当用户回到家时门自动打开,整个过程应用都是后台运行。当用户离开家时,iOS
设备与门锁断开连接。这是应用只简单调用connectPeripheral:options:
,因为连接没有超时,iOS
设备将在用户回到家时重新连接上。
假设用户离开家好几天,并假设app
被系统终止,应用将无法在用户回到家时重连门锁,这时用户将无法开门。对于这类应用,很重要的一点要能够继续使用蓝牙执行长时事件,如管理活动和悬停连接。
- 1、状态保存和还原
因为状态保存和还原是蓝牙内在支持的,你的应用可选择支持这一特征来让系统保存central
和peripheral manager
的状态,并继续执行蓝牙任务,即使你的应用不在运行。当任务完成,系统重新激活应用到后台,让你的应用有机会还原状态并处理事件。上面说的家庭安全应用,系统可以管理连接请求,并在用户回到家重新连接上蓝牙时重新激活应用来处理centralManager:didConnectPeripheral:
代理回调。
蓝牙库支持状态保存和还原,支持central
角色,peripheral
角色。
当应用实现central
角色并增加支持状态保存和还原,系统就会在终止应用释放内存前保存central manager
对象的状态,如果应用有多个central managers
,你可选择哪些对象你希望系统为你维护。对于CBcentralManger
对象,系统如此维护:
- 1,
central manager
扫描的services
和对应的options
- 2,已连接的和未连接上的
peripherals
- 3,订阅的
characteristics
当应用实现peirpheral
角色的应用类似处理。对于CBPeripheralManager
对象,系统这样维护:
- 1,广播的数据
- 2,
peripheral manager
发布到设备数据库的services
和characteristic
- 3,那些订阅了你
characteristics
的值得centrals
当应用被系统重新激活到后台,假如应用之前有发现peripheral
,你可以重新创建应用的central
和peripheral manager
,并还原他们的状态。后面将继续说明如何利用状态保存与还原。
- 2 增加支持状态保存与还原
这一特性是可选的,增加步骤如下:
- 1,(必须)在创建和初始化时选择支持状态保存和还原。
Opt In to State Preservation and Restoration
这一节将更详细描述- 2,(必须)在应用被系统唤醒时复原
central
或peripheral manager
对象。Reinstantiate Your Central and Peripheral Managers
这里将继续描述- 3,(必须)实现还原代理方法。
Implement the Appropriate Restoration Delegate Method。
这里将继续说明- 4,(可选)更新
central
和peripheral managers
的初始化过程。Update Your Initialization Process。
这里将继续说明
- 2.1
Opt In to State Preservation and Restoration
(选择支持状态保存和还原)
在创建和初始化时,提供唯一的还原id
。还原id
是字符串,对于蓝牙库和应用来说,还原id
是用来标记central
或peripheral manger
的。你的代码只关心这个字符串,但这个字符串告诉蓝牙库需要保存被标记对象的状态。蓝牙库只保存那些有标记还原id
的对象的状态。
假如,选择支持状态保存和还原的应用只有一个CBCentralMnager
对象实例实现了central
角色,那么在初始化时初始化options
中增加CBCentralManagerOptionRestoreIdentifierKey
键,并赋值还原id
。
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{ CBCentralManagerOptionRestoreIdentifierKey: @"myCentralManagerIdentifier" }];
peripheral manager
的处理也是类似的,key
是CBPeripheralManagerOptionRestoreIdentifierKey
注意:因为应用可以有多个CBCentralManager
和 CBPeripheralManger
实例。注意每个还原id
都是唯一的,这样系统才能区分开来。
- 2.2
Reinstantiate Your Central and Peripheral Managers
(复原central
和peripheral manager
)
当应用被系统唤醒,你需要做的第一件事是使用还原id
复原central and peripheral manager
。如果应用中只有一个central or peripheral manager
,并且在应用的整个生命周期中存在,那么就简单了。
如果应用使用多个central or peripheral manager
或如果应用使用的manager
不是在app
的整个生命周期中存在,那么应用需要知道哪些managers
需要复原。在实现application:didFinishLaunchingWithOptions:
这个代理方法时,通过使用参数launchoptions
中的键(UIApplicationLaunchOptionsBluetoothCentralsKey or UIApplicationLaunchOptionsBluetoothPeripheralsKey)
可以获得应用在终止时为我们保存的还原id
列表。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
...
}
有了还原id
列表后,就可以复原出central manager
对象了。
注意:当应用被激活时,系统只提供那些应用终止时有蓝牙任务的central and peripheral managers
的还原ids
。
- 2.3
Implement the Appropriate Restoration Delegate Method
(实现还原代理方法)
在重新创建central and peripheral managers
之后,需要通过蓝牙系统还原他们的状态。对于central managers
,要实现centralManager:willRestoreState:
代理方法,对于peripheral managers
实现peripheralManager:willRestoreState:
方法。
重要:对于使用状态保存和还原特性的应用,应用被激活到后台的第一个代理调用是centralManager:willRestoreState:
and peripheralManager:willRestoreState:
。对于未使用这一特性的应用,第一个代理调用是centralManagerDidUpdateState:
and peripheralManagerDidUpdateState:
。
// 应用从后台恢复到前台的时候,会和系统蓝牙进行同步,调用此方法
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)dict{
NSArray *peripherals = state[CBCentralManagerRestoredStatePeripheralsKey];
// 如果数组不为空,这里直接设置代理,发现服务
if(peripherals.count != 0){
CPeripheral *peripheral = [peripherals firstObject];
Self.peripheral = peripheral; //强引用一下
Peripheral.delegate = self; // 设置代理
[peripheral discoverServices:@[]];// 服务数组
}
}
在这些代理中,最后一个参数是dictionary
,包含了应用被终止时managers
的信息。可用键值参考 Central Manager State Restoration Options constants in CBCentralManagerDelegate Protocol Reference and the Peripheral_Manager_State_Restoration_Options constants in CBPeripheralManagerDelegate Protocol Reference
CBCentralManagerRestoredStatePeripheralsKey // 返回一个中心设备正在连接的所有外设数组
CBCentralManagerRestoredStateScanServicesKey // 返回一个中心设备正在扫描的所有服务UUID的数组
CBCentralManagerRestoredStateScanOptionsKey // 返回一个字典包含正在被使用的外设的扫描选项
要还原CBCentralMnager
对象的状态,要使用centralManager:willRestoreState:
方法中dictionary
的键值对。举个例子,假如centralmanger
对象在app
被终止时有acitve
或pending
连接,系统会继续管理他们。就像下面代码所示,可以使用CBCentralManagerRestoredStatePeripheralsKey
键从dictionary
中获取所有设备的列表,这些设备就是central manger
已连接或正在连接的设备。
如何使用这个列表要看具体情况。比如,如果应用要维护central manger
已发现peripherals
的列表,你可能就需要利用到它。参见Connecting to a Peripheral Device After You’ve Discovered It,
请注意在需要给peripheral
设置相应的代理。
对于CBPeripheralManager
对象,也需要类似的处理,相应的代理方法是peripheralManager:willRestoreState:
。
- 2.4
Update Your Initialization Process
(更新你的初始化进程)
在前面的三个步骤之后,你可能想知道central and peripheral manager
的初始化进程。虽然这是一个可选步骤,但如果想让你的应用跑起来更流畅,这可是很重要的。假设应用在检索peripheral
的服务时被终止。当应用还原后,它不知道这个过程到底进行到哪一步了。你也想知道从哪一步继续。
举例,当在centralManagerDidUpdateState:
方法中初始化你的应用时,你可以查到在应用被终止时你是否成功发现被还原peripheral
的某个service
,如下:
NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj,NSUInteger index, BOOL *stop) {
return [obj.UUID isEqual:myService];
}];
if (serviceUUIDIndex == NSNotFound) {
[peripheral discoverServices:@[myService]];
...
}else { // 应用在被终止前已搜索到`service`,那么你需要检查时候搜索到你要的`characteristics`
}
如上,如果系统在应用发现service
之前终止它,那么开始搜索peripheral
的数据,使用discoverServices:
搜索。如果应用在被终止前已搜索到service
,那么你需要检查时候搜索到你要的characteristics
,(如果有订阅,也检查是否已订阅)。通过检查初始化过程,可以确保在这时调用到最合适的方法。
五、与peripheral
通信的最佳实践
5.1 与peripheral
通信的最佳实践
蓝牙库使得central
端的一些事情的处理变得透明。也就是你的应用可以实现central
端的大部分事情,如搜索和连接设备,检索和交互peripheral
的数据。这一章将提供开发指引及如何最佳实现。
5.2 永远记住需要使用无线电并且会消耗电量
当开发应用需要用到蓝牙低功耗设备,请记住蓝牙低功耗通讯需要使用你设备的无线电来传输信号。同时其他形式的无线通讯也可能使用设备的无线电—比如,wi-fi,经典蓝牙,甚至其他使用蓝牙低功耗的应用
—所以,尽量让你的应用尽少使用无线电。
在开发iOS
应用中,尽少使用无线电是非常重要的,因为无线电的使用会减少电池的寿命。下面的指引将帮助你如何以更好的方式使用无线电,从而使你的应用表现更佳,更省电。
####### 5.2.1 只有在需要的时候才搜索设备
当你调用CBCentralManager
的scanForPeripheralsWithServices:options:
方法来搜索(发现)当前正在发送广播的peripheral
,这时你正在使用你设备的无线电来监听广播设备,直到你显示的停止它。
除非你需要搜索更多的设备,否则在你搜索到所需要的设备后,及时的停止搜索。使用CBCentralManager
的stopScan
方法停止搜索设备,参见Connecting to a Peripheral Device After You’ve Discovered It.
####### 5.2.2 只有在需要的时候才添加CBCentralManagerScanOptionAllowDuplicatesKey
搜索参数
peripheral
设备一秒钟可能发送多个广播包。在搜索peripheral
时,默认情况下,多次搜到的广播包只会触发一次事件代理。结果就是不管收到多少个广播包,central manager
对一个peripheral
只会触发centralManager:didDiscoverPeripheral:advertisementData:RSSI:
方法一次。但是如果peripheral
的广播数据变了,也会重新触发代理事件。
如果你不喜欢这种默认的方式,你可以在搜索时给搜索方法scanForPeripheralsWithServices:options:
添加CBCentralManagerScanOptionAllowDuplicatesKey
参数。 这样的话,就可以每次收到广播包信息都触发消息。在某些情况下,我们需要这么做。比如说需要检查设备是在靠近还是在远离(通过RSSI值判断)。所以,请记住添加这个参数会消耗电量,并影响性能,只有在确定需要的时候才使用这个参数。
####### 5.2.3 明智的检索peripheral
的数据
peripheral
设备可能会有多个services
和characteristics
。如果搜索全部的services
和characteristics
会浪费电量,对应用性能也不好。所以我们应该只搜索那些我们需要的services
和characteristics
。
[peripheral discoverServices:@[firstServiceUUID, secondServiceUUID]];
在搜索到需要的services
之后,接着就搜索需要的characteristics
。同样的,使用CBUUID
封装好需要的characteristics
,使用CBPeripheral
的discoverCharacteristics:forService:
方法开始搜索。
####### 5.2.4 订阅经常改变的characteristic
值
在Retrieving the Value of a Characteristic
谈过,有两种方式可以获得characteristic
的值。
- 1,在需要的时候使用readValueForCharacteristic: 轮询characteristic的值
- 2,使用setNotifyValue:forCharacteristic: 订阅这个characteristic。
可能的话,最好采用订阅的方式,特别是在characteristic
的值经常变的情况下。至于如何订阅参见Subscribing to a Characteristic’s Value.
####### 5.2.5 当获得全部所需数据后断开设备连接
当连接已不再需要的时候,断开连接,这样有助于降低无线电的使用。下面两种情况,你应该断开与peripheral
的连接。
- 1,你所订阅的特征值已不再发通知。(
可以通过characteristic的isNotifying属性得知
)- 2,你已获得了全部所需要的数据。
这两种情况下,取消订阅并断开连接。通过setNotifyValue: forCharacteristic:
设置第一个参数 为NO
来取消订阅,通过CBCentralManager 的cancelPeripheralConnection:
方法取消连接。如下:
取消订阅
[self.peripheral setNotifyValue:YES forCharacteristic:characteristic]; // NO 取消订阅
断开连接
[myCentralManager cancelPeripheralConnection:peripheral];
注意:cancelPeripheralConnection:
这个方法是非阻塞的,所以其他正在进行的通讯可能完成也可能因此没完成。因为其他应用可能仍保持跟这个设备的连接,所以当前的取消连接并不能保证底层就立即断开物理上的连接。但从应用本身看来,这个设备已经被认为是断开连接了,central manager
对象也对触发代理的centralManager:didDisconnectPeripheral:error:
方法。
5.3 重新连接peripherals
有三种方式可以重连
- 1,重新获取已发现的设备列表(搜索到的或是连接过的设备),使用
retrievePeripheralsWithIdentifiers:
。如果列表中有想要寻找的设备,那么发起连接。参见Retrieving a List of Known Peripherals
- 2,重新获取当前连接着的设备列表,使用
retrieveConnectedPeripheralsWithServices:
。 如果列表中有想要寻找的设备,发起本地连接,使得应用与之连接上。参见Retrieving a List of Connected Peripherals
。- 3,使用
scanForPeripheralsWithServices:options:
重新搜索设备,如果找到了就去连接。参见Discovering Peripheral Devices That Are Advertising and Connecting to a Peripheral Device After You’ve Discovered It.
实际情况下,你可能不想每次连接都先去搜索设备,你可能更希望使用另外两种方式。如图所示,一种先尝试使用另两种方式连接的方法
注意:采用什么方式,什么顺序依情况而定,比如你可以不采用第一种方式,也可以同时使用前两种并行的方式。
####### 5.3.1 Retrieving a List of Known Peripherals
(获取已知设备列表)
当你第一次发现某个peripheral
,系统会为他生成一个identifier
(一个NSUUID
对象),你可以保存这个identifier
(比如用NSUserDefaults
保存),然后在后面需要用的时候使用retrievePeripheralsWithIdentifiers:
重新连接。
knownPeripherals = [myCentralManager retrievePeripheralsWithIdentifiers:savedIdentifiers];
central manager
尝试匹配你传入的identifiers
,并返回CBPeripheral
对象。如果没有匹配的设备,array
将为空,这时你需要使用另外两种方式。如果不为空,那么让用户选择要连接到哪个设备。
当用户选择了要连接的设备,则调用connectPeripheral:options:
方法来尝试连接。如果设备被连接上,则会触发代理消息centralManager:didConnectPeripheral:
。
注意:可能有多种原因导致设备不能被连接上。比如,设备不在附近。还有一种可能,一些低功耗蓝牙使用随机设备地址,在重新连接时,它的地址可能已经变了。因此,即使设备就在附近,设备的地址也已经变了,这种情况下,你想要连接的设备与实际设备已经不匹配了。这种情况,你只有重新搜索了。更多详情请参考Bluetooth 4.0 specification, Volume 3, Part C, Section 10.8 and Bluetooth Accessory Design Guidelines for Apple Products.
####### 5.3.2 Retrieving a List of Connected Peripherals
(获取已连接蓝牙)
另一种重新连接的方法是检查你想要连接的设备是否已经连接到系统了(可能其他应用正连着呢)。你可以使用retrieveConnectedPeripheralsWithServices:
方法获取CBPeripheral
对象Array
。
因为当前可能有多个peripheral
连接着系统,你可以传递CBUUID
对象(注意是service
的UUID
)的Array
。这样他将只返回当前连接着的,并且包含array
中所有serives
的peripheral
。如果没有符合条件的,则返回的array
为空,这时你需要使用别的方法。如果array
不为空,那么让用户来选择。
如果用户找到并选择了所要的peripheral
,使用connectPeripheral:options:
把它连接到你的应用。当连接建立,会触发代理centralManager:didConnectPeripheral:
这时说明连接成功了。
六、作为Peripheral
端的最佳方式
作为Peripheral
端的最佳方式
与central
端类似,蓝牙库让你能够实现peripheral
角色的多方面的控制。本章节提供指引,并讲述如何使用。
6.1 广播是实现peripheral
建立连接的一个重要部分。
####### 6.1.1 注意广播数据的限制
在CBPeripheralManager
的startAdvertising:
方法中,通过dictionary
参数传递peripheral
的广播数据。创建广播字典时,时刻记住有哪些限制。
- 1.虽然广播包通常情况下可以有多种的信息,但你只能广播设备名称和
services
的uuid
。也就是说,在你创建广播字典时,你只能设置这两个key: CBAdvertisementDataLocalNameKey and CBAdvertisementDataServiceUUIDsKey 。设置其他key
将导致错误。- 2.广播数据的空间也有限制。当应用在前台时,这两个
key
的值最多有28 字节
可用。搜索时,如果这个空间用完,另外还有10个字节
的响应数据可以用来标识设备名称。超出规定空间的service uuids
将会被放到“Overflow”
区域,在iOS
设备显示搜索他们时会被搜索到。当设备在后台时,local name
不会被广播,同时所有的services uuids
都放入overflow
区域。
注意:这个限制的数值不包括2字节
的头部信息。详细格式信息参见Bluetooth 4.0 specification, Volume 3, Part C, Section 11.
为了使你的数据符合空间要求,请使用主要的services的uuid
。
####### 6.1.2 只在需要的时候广播数据
广播数据会使用设备的无线电(当然了还有电池),所以只有在你希望被连的时候才广播数据。一旦连接上,这个设备就可以检索和交互数据了,而不需要你在广播数据了。因此,为了少使用无线电,增加应用响应性能,保护电池,请停止发送广播。使用stopAdvertising
方法停止,如下
[myPeripheralManager stopAdvertising];
####### 6.1.3 让用户决定什么时候广播
往往只有用户才最知道什么时候需要发送广播。如果附近没有蓝牙设备,你发送广播也是没有意义的。既然应用本身不知道何时需要发送广播,那就提供接口让用户来决定。
6.2 配置characteristic
创建可变characteristic
之后,我们修改其属性,值,和访问权限。这些设置决定连接的central
如何访问和与之交互数据。虽然不同的属性和访问权限由各自app
决定,但当你要完成下面两项任务时,这里提供相关指引:
- 1,允许
central
订阅你的characteristics
- 2,如果未配对,请保护敏感数据
6.3 设置characteristic
,使之支持通知
在Subscribe to Characteristic Values That Change Often
中有描述,推荐在characteristic
的值经常变化时,使用订阅的方式。如果可能的话,尽量让central
端能够采用订阅的方式。
创建的时候,通过CBCharacteristicPropertyNotify
设置characteristic
的属性,使之支持订阅。
myCharacteristic = [[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
这样,characteristic
会是可读,可被订阅的。
6.4 要求配对连接才能访问敏感数据
实际情况下,你可能需要发送敏感数据。比如数据中有姓名,email
地址等,你希望只有可信任的设备才能访问这些数据。
要保证只有可信任的设备才能访问敏感数据,可以通过设置合适的characteristic
属性和访问权限。比如上面创建的例子,可做如下修改:
emailCharacteristic = [[CBMutableCharacteristic alloc] initWithType:emailCharacteristicUUID properties:CBCharacteristicPropertyRead| CBCharacteristicPropertyNotifyEncryptionRequired value:nil permissions:CBAttributePermissionsReadEncryptionRequired];
在这里characteristic
被配置成只允许可信任设备访问和订阅他的值。当central
想要连接和订阅这个characteristic
的值时,蓝牙库就会尝试配对来建立安全连接。
配对过程完成后,peripheral
就会认为这个central
是一个可信任设备,并允许central
访问其加密过的值。
参考文献
参考文献