iOS蓝牙开发实践

3,098 阅读27分钟

蓝牙的类型分为经典蓝牙(BR/EDR)和低功耗蓝牙(BLE)。在iOS平台下经典蓝牙技术的开发需要设备加入Apple的MFi认证,具体流程参见:加入MFi。进行MFi认证的流程比较复杂,设备造价成本也较高,在没有特殊要求的情况下一般不采用该方式。本章节主要讲述的是iOS平台下BLE蓝牙技术的开发。

Core Bluetooth简介

Core Bluetooth 框架 (CoreBluetooth.framework)提供工具类供iOS和MacOS系统下的app和BLE蓝牙设备进行通讯。app可发现、连接、设备,并和设备进行数据交互,Mac和iOS系统的设备也可作为蓝牙外围设备和其他设备进行通讯。

Core Bluetooth 框架是对蓝牙协议规范的抽象,封装了蓝牙协议的功能,使用它可更加容易的开发出和蓝牙设备进行交互的app。它是基于蓝牙协议规范的,因此采用了规范中的一些概念和术语。

中央和外围设备及其在蓝牙通信中的作用

蓝牙通信过程中两个主要的参与者:中央(Central)和外围(Peripheral)设备。这种体系结构某种程度上参考的是传统的客户端-服务器架构(C/S)。外围设备通常是具有其他设备所需的数据,而中心设备则使用外围设备提供的信息来完成任务。外围设备相当于提供数据的服务器,中心设备相当于获取数据的客户端。下图就清晰的表示了它们之间的关系:

中央设备发现和连接对外广播的外围设备

外围设备通过发送广播包的形式对外广播数据。广播包包含外围设备必须提供的有效信息,例如设备的名称和主要功能。在蓝牙技术中,广播是外围设备对外呈现的主要方式。

中央设备可以扫描和监听它们感兴趣的正在广播数据的外围设备,并寻找合适的机会进行连接。下图展示中央设备可以对其扫描发现到的任何设备发起连接请求:

外围设备提供数据的数据结构

在BLE蓝牙技术中app和设备之间传输的数据是封装在服务和特性中的,服务是完成设备相关功能的数据或者行为的集合。例如,心率检测设备的一项服务可能就是用来公开设备的检测到的新心率数据。服务是由其包含的特性和其他服务组成,特性提供有关服务的更加详情的信息。例如,刚刚提到心率服务可能包含两个特性,一个用来描述心率传感器在预期在身体中的位置,一个用来传输测量得到的心率数据。下图展示了心率检测器的服务-特性结构:

中央设备和外围设备的交互

中央设备和外围设备建立连接之后,它可以发现设备提供的所有服务和其包含的特性。中央设备和外围设备的交互是通过向外围设备的服务特性中读取或者写入值来实现的。

CoreBluetooth开发流程

使用CoreBlurtooth的开发方式通常是将iPhone或者Mac作为本地中央设备来与手环或者蓝牙音响等远程外围设备交互数据,因此,我们看这种开发场景下,设备相关的对象在CoreBluetooth中是如何表示的:

上图呈现了Core Bluetooth framework如何表示本地中心设备和远程外围设备,当本地设备作为中央设备时,中央设备由CBCentralManager对象表示。用来管理发现和连接的远程设备(由CBPeripheral对象表示),包括扫描、连接到发送广播的外围设备、断开远程设备等功能。

当需要和远程设备(由CBPeripheral对象表示)进行数据交互,需要处理GATT中的服务和特性,在Core Bluetooth framework中它们由CBServerceCBCharacteristic对象表示。下图说明了远程外围设备的服务和特征的基本结构:

了解了CoreBluetooth的开发场景和主要的类后,下面直接上代码。

初始化

Core Bluetooth framework中的本地中央设备是由CBCentralManager对象表示,在进行低功耗蓝牙交互之前需要生成一个中央设备管理器的实例,需调用该方法initWithDelegate:queue:options:

  •    myCentralManager =
    
            [[CBCentralManager alloc] initWithDelegate:self queue:nil options:nil];
    
    该方法有两个参数,具体用法如下:
    

    第一个参数是CBCentralManagerDelegate的代理对象,主要用来接收蓝牙相关的事件。比如,当蓝牙状态更新时会触发centralManagerDidUpdateState:方法。扫描到设备、连接到设备、设备被断开等蓝牙行为被触发时都可以被该对象接收到。

  • 第二个参数是分发队列,指的是上述描述的这些事件是在那个队列中分发的。如果为nil,则表示在主队列中分发(主线程)。要想蓝牙交互过程中的数据在子线程中进行,该参数就不能为nil。

扫描

发起扫描

设备管理器初始化好之后的首要任务就是发现设备。发现附近正在广播的设备调用管理器对象的scanForPeripheralsWithServices:options:方法:

[myCentralManager scanForPeripheralsWithServices:nil options:nil];

该方法有两个参数,具体用法如下:

  • serviceUUIDs:CBUUID对象的数组,用来表示一序列服务,只有支持的服务在这个列表范围内的设备才能被扫描到。当该参数为nil时表示所有设备都可以被扫描到。

  • options:扫描的配置项,有两项可以配置 可参见Peripheral Scanning Options.

    • CBCentralManagerScanOptionAllowDuplicatesKey,值为NSNumber对象,表示一个设备是否重复接收广播事件。当值为ture时则表示每收到一个广播包就生成一个发现事件并发送。为false时则把同一设备的多个广播发现合并成一个发现事件,默认为flase。使用样例:

    • NSDictionary *optionDic = 
      [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
      forKey:CBCentralManagerScanOptionAllowDuplicatesKey]; 
      
      [self.cbCentralManager scanForPeripheralsWithServices:nil                                              options:optionDic];  
      

      CBCentralManagerScanOptionSolicitedServiceUUIDsKey,值为一个UUID的数组,表示你想要接受的UUID的列表。

获取扫描结果

中央设备管理器扫描发现设备后,将会调用委托对象(delegage)的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;      
... 

停止扫描

如果希望连接到多个设备,可以用数组来管理这些设备(NSArray),一旦找到了要连接的所有外围设备,请停止扫描其他设备以节省电量,停止扫描的方法:

[myCentralManager stopScan];

连接

发起连接

当发现了你感兴趣的广播服务的设备后,可以通过中央设备管理器调用connectPeripheral:options:方法来连接它:

[myCentralManager connectPeripheral:peripheral options:nil];

options参数请参考这里

连接成功

如果连接成功,中央设备设备管理器让其代理对象调用`centralManager:didConnectPeripheral:方法,在和该设备进行交互之前,需要设置其委托对象以便收到适当的回调,回调方法参见CBPeripheralDelegate:

- (void)centralManager:(CBCentralManager *)central    
didConnectPeripheral:(CBPeripheral *)peripheral {      

NSLog(@"Peripheral connected");      
peripheral.delegate = self;      
... 

查找服务

开始查找

在和外围设备建立连接之后,你需要何其交互数据,和设备交互数据的第一步就是找到设备可用的服务。由于外围设备的广播包数据大小受限制,所以你可能会发现外围设备提供的服务比其广播(广播包中)的要多。设备对象调用discoverServices:方法可以发现设备提供的所有服务,比如:

- (void)centralManager:(CBCentralManager *)central    
didConnectPeripheral:(CBPeripheral *)peripheral {  

NSLog(@"Peripheral connected");      
peripheral.delegate = self;     
NSLog(@"discove reripheral Services");      
[peripheral discoverServices:nil];      
... 

查找服务成功

当设备的服务被发现后,设备将会让其委托对象(CBPeripheralDelegate对象)调用peripheral:didDiscoverServices:方法。Core Bluetooth framework 会为每一个被发现的服务,创建一个CBService对象,放到数组中作为CBPeripheral对象的属性。下方的代码将展示,通过实现委托方法来访问发现的服务列表:

- (void)peripheral:(CBPeripheral *)peripheral  
didDiscoverServices:(NSError *)error {         

for (CBService *service in peripheral.services) {          
NSLog(@"Discovered service %@", service);          
...      
}      
... 

查找特征

开始查找

找到设备提供的服务之后,第二步就需要去发现该服务提供的特征。查找服务的特征需要设备对象调用 discoverCharacteristics:forService: 方法,比如:

- (void)peripheral:(CBPeripheral *)peripheral  
didDiscoverServices:(NSError *)error { 

for (CBService *service in peripheral.services) {  

if ([service.UUID.UUIDString isEqual:"interestingService UUID"]){  

CBService * interestingService = service;                   
NSLog(@"Discovering characteristics for service %@", interestingService);                    [peripheral discoverCharacteristics:nil forService:interestingService];           }      
}      
... 

查找特征成功

当服务的特征被发现时,设备使用其委托对象(CBPeripheralDelegate对象)调用peripheral:didDiscoverCharacteristicsForService:error:方法。Core Bluetooth framework 会为每一个被发现的特征,创建一个CBCharacteristic对象,放到数组中作为CBService对象的属性。下方的代码将展示,通过实现委托方法来访问发现的服务特征列表:

- (void)peripheral:(CBPeripheral *)peripheral

didDiscoverCharacteristicsForService:(CBService *)service

             error:(NSError *)error {



    for (CBCharacteristic *characteristic in service.characteristics) {

        NSLog(@"Discovered characteristic %@", characteristic);

        ...

    }

    ...

从外围设备中获取数据

特征中包含的值代表设备服务对外提供的信息.例如,温度计服务的温度测量特性可以具有指示摄氏温度的值。以通过直接读取或订阅来获取该特征的值。

读取特征中的值

在设备服务中发现了特征之后,可以调用设备对象的readValueForCharacteristic:方法来尝试读取特征中的值。类似:

- (void)peripheral:(CBPeripheral *)peripheral

didDiscoverCharacteristicsForService:(CBService *)service

             error:(NSError *)error {

    for (CBCharacteristic *characteristic in service.characteristics) {

        if ([character.UUID.UUIDString containsString:@"interestingCharacteristic uuid"]) {

                 CBCharacteristic *interestingCharacteristic = characteristic;

             NSLog(@"Discovered target characteristic %@", interestingCharacteristic);

             [peripheral readValueForCharacteristic:interestingCharacteristic];

        ...

        }

    }

当特征值的获取成功之后,设备对象会使其委托对象(CBPeripheralDelegate对象)调用 peripheral:didUpdateValueForCharacteristic:error: 方法,并将值作为CBCharacteristic对象的属性。可以在委托方法中访问特性的value属性,来获取特征的值,类似:

- (void)peripheral:(CBPeripheral *)peripheral  
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic 
error:(NSError *)error {       

NSData *data = characteristic.value;      
// parse the data as needed      
... 

订阅特征中的值

readValueForCharacteristic:方法只能获取静态数据,也就是调用一次获取一次结果。要想获取随时变化的特征值,比如随时变化的心率,就必须订阅特征值,当值发生变化时会收到外围设备发送的通知。

设备对象调用setNotifyValue:forCharacteristic:方法来订阅指定特征的值,第一个参数设置为YES,类似:

 - (void)peripheral:(CBPeripheral *)peripheral

didDiscoverCharacteristicsForService:(CBService *)service

             error:(NSError *)error {

    for (CBCharacteristic *characteristic in service.characteristics) {

        if ([character.UUID.UUIDString containsString:@"interestingCharacteristic uuid"]) {

                 CBCharacteristic *interestingCharacteristic = characteristic;

             NSLog(@"Discovered target characteristic %@", interestingCharacteristic);

             [peripheral readValueForCharacteristic:interestingCharacteristic];

                          [peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

        ...

        }

    }

订阅(获取取消订阅)了特征的值之后,设备对象将使用它的委托对象(CBPeripheralDelegate对象)调用peripheral:didUpdateNotificationStateForCharacteristic:error:方法。可以实现该委托访问来处理订阅成功或者失败的情况,类似:

- (void)peripheral:(CBPeripheral *)peripheral

didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic

             error:(NSError *)error {



    if (error) {

        NSLog(@"Error changing notification state: %@",

           [error localizedDescription]);

    }

    ...

成功订阅了特征值之后,特征值变化后,设备对象会使其委托对象(CBPeripheralDelegate对象)调用 peripheral:didUpdateValueForCharacteristic:error: 方法来获取更新后特征值,和上面描述的读取特征一样需要实现该委托方法,并在该方法中获取更新后的值。

向外围设备发送数据

向外围设备写入(发送)数据,也是通过向特征中写入值来实现的。特性有可写的属性才支持写入值,否则会报错。使用外围设备对象调用 writeValue:forCharacteristic:type: 方法来实现向其发送数据, 类似:

NSLog(@"Writing value for characteristic %@", interestingCharacteristic);

[peripheral writeValue:dataToWrite forCharacteristic:interestingCharacteristic

        type:CBCharacteristicWriteWithResponse];

如果要知道发送数据的结果,将上述方法中的type参数设置为CBCharacteristicWriteWithResponse后,当数据发送成功或者失败后,设备对象会使其委托对象(CBPeripheralDelegate对象)调用peripheral:didWriteValueForCharacteristic:error:方法。可以实现该委托方法来处理发送数据后的结果,类似:

- (void)peripheral:(CBPeripheral *)peripheral  
didWriteValueForCharacteristic:(CBCharacteristic *)characteristic
error:(NSError *)error {        

if (error) {          
NSLog(@"Error writing characteristic value: %@",              
[error localizedDescription]);      
} else {          
NSLog(@" writing characteristic value success")     
} 

适用于iOS应用的核心蓝牙后台处理

对于iOS应用,了解你的应用是在前台运行还是在后台运行至关重要。应用程序在后台与前台的行为必须有所不同,因为iOS设备上的系统资源受到更多限制。有关iOS上的后台操作的整体讨论,请参阅 App Programming Guide for iOS **中的 Background Execution

默认情况下,在应用程序处于后台或挂起状态时,在中心和外围设备上的许多常见Core Bluetooth任务都被禁用。也就是说,你可以声明你的应用程序支持Core Bluetooth后台执行模式,以允许你的应用程序从挂起状态唤醒以处理某些与Bluetooth相关的事件。即使你的应用不需要全方位的后台处理支持,也可以在发生重要事件时要求系统发出警报。

即使你的应用支持一种或两种Core Bluetooth后台执行模式,也无法永远运行。在某个时候,系统可能为了当前运行的程序而终止你的处于后台的应用程序,从而导致所有活动或挂起的连接丢失。从iOS 7开始,Core Bluetooth支持保存中心和外围设备管理器对象的状态信息,并在应用程序启动时恢复该状态。你可以使用此功能来支持涉及蓝牙设备的长期操作。

仅前台应用

与大多数iOS应用程序一样,除非你请求执行特定后台任务的权限,否则你的应用程序在进入后台状态后不久便会转换为暂停状态。处于挂起状态时,你的应用无法执行与蓝牙相关的任务,也无法感知任何与蓝牙相关的事件,直到恢复到前台为止。

在中心设备方面,仅前台应用程序(未声明支持两种核心蓝牙后台执行模式的应用程序)在后台或挂起时无法扫描并发现广告外设。在外围设备方面,广告被禁用,并且任何尝试访问该应用程序已发布服务之一的动态特征值的中心都将收到错误消息。

根据使用情况,此默认行为可能以多种方式影响你的应用程序。举例来说,假设你正在与当前连接的外围设备上的数据进行交互。现在,假设你的应用程序进入了挂起状态(例如,由于用户切换到另一个应用程序)。如果在应用暂停期间与外围设备的连接丢失,则在应用恢复到前台之前,你将不会发现任何断开连接。

利用外围连接选项

仅在前台的应用程序处于挂起状态时发生的所有与蓝牙相关的事件均被系统加入队列,并仅在恢复到前台时才传递给该应用程序。不过核心蓝牙提供了一种在某些中心角色事件发生时提醒用户的方法,然后,用户可以使用这些警报来确定特定事件是否需要使应用程序重新回到前台。

通过调用CBCentralManager类的connectPeripheral:options:方法以连接到远程外围设备时,可以通过包括以下外围设备连接选项之一来利用这些警报:

  • CBConnectPeripheralOptionNotifyOnConnectionKey——如果你希望系统在建立成功的连接后被挂起,并且你希望系统显示给定外围设备的警报,则包括此键
  • CBConnectPeripheralOptionNotifyOnDisconnectionKey——如果你希望系统在给定外围设备显示断开连接警报的情况下断开应用程序的连接,请包含此密钥
  • CBConnectPeripheralOptionNotifyOnNotificationKey——如果你希望系统在应用程序当时被暂停的情况下显示从给定外围设备收到的所有通知的警报,请包含此密钥

有关外围设备连接选项的更多信息,请参见Peripheral Connection Options常量,在CBCentralManager类参考中详细介绍。

核心蓝牙后台执行模式

如果你的应用需要在后台运行以执行某些与蓝牙相关的任务,则必须在其信息属性列表(Info.plist)文件中声明其支持Core Bluetooth后台执行模式。当你的应用声明此消息时,系统会将其从挂起状态唤醒,以允许其处理与蓝牙相关的事件。此支持对于与可定期发送数据的低功耗蓝牙设备进行交互的应用(例如心率监测器)非常重要。

应用程序可以声明两种核心蓝牙后台执行模式,一种是实现中心角色的应用程序,另一种是实现外围角色的应用程序。如果你的应用程序同时实现这两个角色,则可能会声明它支持两种后台执行模式。通过将UIBackgroundModes键添加到Info.plist文件并将该键的值设置为包含以下字符串之一的数组来声明蓝牙核心背景执行模式:

bluetooth-central——应用程序使用Core Bluetooth框架与低功耗蓝牙外围设备进行通信。

bluetooth-peripheral——该应用程序使用Core Bluetooth框架共享数据。

bluetooth-central 后台执行模式

当实现中心角色的应用程序在其Info.plist文件中包含带有蓝牙中心值的UIBackgroundModes键时,Core Bluetooth框架将允许你的应用程序在后台运行以执行某些与蓝牙相关的任务。当你的应用程序在后台运行时,你仍然可以发现并连接到外围设备,以及探索外围设备数据并与之交互。此外,当调用任何CBCentralManagerDelegateCBPeripheralDelegate委托方法时,系统都会唤醒你的应用,从而使你的应用能够处理重要的中心角色事件,例如建立连接或断开连接,外围设备发送更新的特征值时,以及中心管理者的状态发生变化时。

尽管你可以在后台运行应用程序时执行许多与蓝牙相关的任务,但请记住,在后台运行时扫描外围设备的操作与在前台运行时不同。特别是当你的应用在后台扫描设备时:

  • 将忽略CBCentralManagerScanOptionAllowDuplicatesKey扫描选项键,并将广告外围设备的多个发现合并为一个发现事件。
  • 如果所有正在扫描外围设备的应用程序都在后台运行,则中心设备扫描广告包的时间间隔会增加。结果,发现广告外围设备可能需要更长的时间。

这些更改有助于最大限度地减少无线电使用并延长 iOS 设备上的电池寿命。

bluetooth-peripheral后台执行模式

若要在后台执行某些外围角色任务,必须在应用的 Info.plist 文件中包含bluetooth-peripheral值的 UIBackgroundModes 键。当此键值对包含在应用的 Info.plist 文件中时,系统将唤醒应用以处理读取、写入和订阅事件。

除了允许将应用唤醒以处理来自连接的中心的读取、写入和订阅请求外,Core 蓝牙框架还允许应用在后台状态时进行广播。也就是说,你应该注意,当应用在后台时,广播的运作方式与应用位于前台时不同,特别是,当你的应用在后台进行广播时:

  • CBAdvertedDataLocalNameKey 广告键被忽略,并且不广播外围设备的本地名称
  • CBAdvertedDataServiceUIDsKey 播发键值中包含的所有服务 UUID 都放置在一个特殊的"溢出"区域中;它们只能通过显式扫描的 ios 设备来发现
  • 如果所有正在广播的应用都在后台,则外围设备发送广告包的频率可能会降低

明智地使用后台执行模式

尽管声明你的应用支持一种或两种核心蓝牙后台执行模式可能需要满足特定的用例,但应始终负责任地执行后台处理。由于执行许多与蓝牙相关的任务需要主动使用 iOS 设备的板载无线电,而无线电使用又会对 iOS 设备的电池寿命产生负面影响,因此尽量减少在后台的量。对于任何蓝牙相关事件唤醒的应用应处理它们并尽快返回,以便可以再次挂起应用。

任何声明支持核心蓝牙后台执行模式的应用都必须遵循以下基本准则:

  • 应用应基于会话,并提供一个界面,允许用户决定何时启动和停止交付蓝牙相关事件
  • 唤醒后,应用有大约 10 秒的时间完成任务。理想情况下,它应该尽快完成任务,并允许自己再次挂起。花费太多时间在后台执行的应用可能会被系统限制或杀死
  • 应用不应将唤醒用作执行与系统唤醒应用原因无关的无关任务的机会

有关应用在后台状态下应如何表现的更一般信息,请参阅 Being a Responsible Background App in App Programming Guide for iOS.

在后台执行长期操作

某些应用可能需要使用核心蓝牙框架在后台执行长期操作。例如,假设你正在为 iOS 设备开发一个家庭安全应用程序,该应用与门锁(配备蓝牙低功耗技术)进行通信。当用户离开家时,应用和锁进行交互,以自动锁定门,并在用户返回时解锁门,而所有应用位于后台。当用户离开家时,iOS 设备最终可能会失去锁的范围,导致与锁的连接丢失。此时,应用程序只需调用CBCentralManager 类的方法 connectPeripheral:option:,并且由于连接请求不会发出时间,当用户返回时,iOS 设备将重新连接。

状态保存和恢复

由于状态保留和恢复内置于 Core Bluetooth中,因此你的应用可以选择加入此功能,以要求系统保留应用的中心和外围管理器的状态,并继续代表他们执行某些与蓝牙相关的任务,即使应用不再运行。当其中一项任务完成后,系统将应用重新启动到后台,并让你的应用有机会恢复其状态并适当处理事件。在上述家庭安全应用的情况下,系统将监视连接请求,并重新启动应用以处理 centralManager:didConnectPeripheral: 当用户返回且连接请求完成时委托回调。

核心蓝牙支持实现中心角色、外围角色或两者同时实现的应用的状态保留和恢复。当你的应用实现中心角色并添加对状态保留和恢复的支持时,系统会保存中心管理器对象的状态,当系统即将终止你的应用以释放内存时(如果你的应用有多个中心管理器,你可以选择要系统跟踪哪些中心管理器)。特别是,对于给定的 CBCentralmanager 对象,系统会跟踪:

  • 中心管理器正在扫描的服务(以及扫描启动时指定的任何扫描选项)
  • 中心管理器尝试连接到或已连接到的外围设备
  • 中心管理器订阅的特征

实现外围角色的应用同样可以利用状态保存和恢复。对于 CBPerphermanager 对象,系统会跟踪:

  • 外围管理器要广播的数据
  • 外围管理器发布到设备数据库的服务和特征
  • 订阅了外围设备的特征值的中心设备

当系统将应用重新启动到后台(例如,由于发现应用正在扫描的外围设备)时,你可以重新设置应用的中心和外围管理器并恢复其状态。以下部分详细介绍了如何利用应用。

添加对状态保存和恢复的支持

Core Bluetooth中的状态保存和恢复是一种选择加入功能,需要应用的帮助才能工作。你可以通过以下过程在应用中添加对此功能的支持:

选择加入状态保存和恢复功能

选择加入状态保存和恢复功能,只需在分配和初始化中心或外围管理器时提供唯一的还原标识符。还原标识符是一个字符串,用于标识核心蓝牙和应用的中心或外围管理器。字符串的值仅对代码很重要,但此字符串的存在告诉 Core Bluetooth,它需要保留标记对象的状态。核心蓝牙仅保留具有还原标识符的对象的状态。

例如,若要选择在仅使用 CBCentralManager 对象的一个实例来实现中心角色的应用中进行状态保存和还原,指定 CBCentralManagerOptionRestoreIdentifierKey 选项属性标识键初始化选项,并在分配和初始化中心管理器时为中心管理器提供还原标识符。

myCentralManager =

        [[CBCentralManager alloc] initWithDelegate:self queue:nil

         options:@{ CBCentralManagerOptionRestoreIdentifierKey:

         @"myCentralManagerIdentifier" }];

尽管上述示例没有演示这一点,但你可以选择在以类似方式使用外围管理器对象的应用中进行状态保存和还原:指定 CBPeripheralManagerOptionRestoreIdentifierKey 初始化选项,并在分配和初始化每个外围设备管理器对象时提供还原标识符。

注意:由于应用可以具有 CBCentralManager 和 CBPerphermanager 对象的多个实例,请确保每个还原标识符是唯一的,以便系统能够正确区分一个中心(或外围)管理器对象和另一个。

恢复你的中心和外围管理器

当系统将应用重新启动到后台时,你需要做的第一件事是使用与首次创建时相同的还原标识符重新修复适当的中心和外围管理器。如果你的应用只使用一个中心或外围管理器,并且该管理器在应用的生存期内存在,则对于此步骤,你无需执行任何操作。

如果你的应用使用多个中心或外围管理器,或者它使用的管理器不在应用生命周期内,则你的应用需要知道在系统重新启动时要重新设置哪些管理器。在实现应用的委托:didFinishLaunchingWithOptions: 方法时,可以使用相应的启动选项键(UIApplicationLaunchOptionBluetoosCentersKey 或 UIApplicationLaunchOptionBluetoothperipersKey)访问系统在应用终止时为应用保留的所有管理器对象的还原标识符的列表。

例如,当系统重新启动应用时,你可以检索系统为应用保留的中心管理器对象的所有还原标识符,例如:

- (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

 

    NSArray *centralManagerIdentifiers =

        launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];

    ...

获得还原标识符列表后,只需循环浏览它并重新修复相应的中心管理器对象。

实现适当的还原委托方法

在重新设置应用中适当的中心和外围管理器后,通过将它们的状态与蓝牙系统的状态同步来还原它们。若要使应用与系统代表其执行操作的速度保持快,(在系统未运行时),必须实现适当的还原委托方法。对于中心管理者,实现 centralManager:willRestoreState: 委托方法;对于外围管理器,实现 peripheralManager:willRestoreState: 委托方法。

重要提示:对于选择加入核心蓝牙的状态保存和恢复功能的应用,这些是第一个方法( centralManager:willRestoreState: peripheralManager:willRestoreState: )在应用重新启动到后台以完成某些蓝牙相关任务时调用。对于不选择加入状态保存的应用(或者启动时没有要恢复)的应用,则首先调用 centralManagerDidUpdateState: peripheralManagerDidUpdateState: 委托方法。

在上述两种委托方法中,最后一个参数是包含有关应用终止时保留的管理器的信息的字典。有关可用字典键的列表,请参阅 CBCentralManagerDelegate Protocol Reference 中的Central Manager State Restoration Options常量和 CBPeripheralManagerDelegate Protocol Reference 中的 Peripheral_Manager_State_Restoration_Options常量。

若要还原 CBCentralManager 对象的状态,请使用 centralManager:willRestoreState: 委托方法中提供的字典的键。例如,如果在应用终止时,你的中心管理器对象有任何活动或挂起的连接,则系统将继续代表应用监视它们。如以下所示,你可以使用 CBCentralManagerRestoredStatePeripheralsKey 字典键获取中心管理器连接到或尝试连接到的所有外围设备(由 CBPeripheral 对象表示)的列表:

- (void)centralManager:(CBCentralManager *)central

      willRestoreState:(NSDictionary *)state {

 

    NSArray *peripherals =

        state[CBCentralManagerRestoredStatePeripheralsKey];

    ...

在上面的示例中,对还原的外设列表进行操作取决于用例。例如,如果你的应用保留了中心管理器发现的外围设备的列表,你可能希望将还原的外设添加到该列表中,以保留对这些外围设备的引用。如发现后连接到外围设备中所述,请确保设置外设的委托,以确保它收到相应的回调。

通过使用外围管理器中提供的字典的键:将恢复状态:委托方法,可以以类似的方式还原 CBPeriphermanager 对象的状态。

更新初始化过程

实施前三个必需步骤后,你可能需要查看更新中心和外围管理员的初始化过程。尽管这是一个可选步骤,但确保应用程序中一切平稳运行可能非常重要。例如,你的应用在探索连接外设的数据时可能已经终止。当应用使用此外围设备还原时,它不会知道在终止时它使发现过程有多远。你需要确保从发现过程中离开的地方开始。

例如,在中心ManagerDidUpdateState:委托方法中初始化应用时,你可以找出是否成功发现了还原的外围设备的特定服务(在应用终止之前),例如:

NSUInteger serviceUUIDIndex =

        [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj,

        NSUInteger index, BOOL *stop) {

            return [obj.UUID isEqual:myServiceUUIDString];

        }];

 

    if (serviceUUIDIndex == NSNotFound) {

        [peripheral discoverServices:@[myServiceUUIDString]];

        ...

如上述示例所示,如果系统在发现服务之前终止了应用,则通过调用 discoverServices:开始探索还原的外围设备的数据。如果你的应用成功发现了该服务,你可以检查是否发现了适当的特征(以及你是否已经订阅了这些特征)。通过以这种方式更新初始化过程,你将确保在正确的时间调用正确的方法。