这是 CoreBluetooth 系列的最后一篇,其他文章可查看:
CoreBluetooth1 初识
CoreBluetooth2 作为 Central 时的数据读写
CoreBluetooth3 作为 Central 时的数据读写(补充)
CoreBluetooth4 作为 Central 时的数据读写(最佳实践)
CoreBluetooth5 作为 Central 时的数据读写(OTA 固件升级与文件传输)
CoreBluetooth6 作为 Peripheral 时的请求响应
CoreBluetooth7 作为 Peripheral 时的请求响应(最佳实践)
对于 iOS app 来说,知道现在是运行在前台和后台是至关重要的。因为当程序挂起后,对资源的使用是相当有限的。关于多任务的介绍,可以看app 开发手册。
默认情况下,Core Bluetooth 是不会在后台运行的(无论是 central 还是 peripheral)。但你也可以配置在 app 收到事件后,从挂起状态唤醒。即使程序不是完全的支持后台模式,也可以要求在有重要事件时接收系统通知。
即使在以上两种情况下(完全允许后台和部分允许后台),程序也有可能不会永远挂起。在前台程序需要更多内存时,被挂起的程序很有可能会被强制退出,那样会断开所有的连接。从 iOS 7 开始,能够先保存状态(无论是 central 还是 peripheral),并在重新打开 app 时还原这些状态。通过这一�特性,就可以做长时间操作了。
运行在前台的 app (Foreground-Only)
除非去申请后台权限,否则 app 都是只在前台运行的,程序在进入后台不久便会切换到挂起状态。挂起后,程序将无法再接收任何蓝牙事件。
对于 central 来说,挂起将无法再进行扫描和搜索 peripheral。对于 peripheral 来说,将无法再发起广播,central 也无法再访问动态变化的 characteristic 数据,访问将返回 error。
根据不同情况,这种机制会影响程序在以下几个方面的运用。你正在读取 peripheral 的数据,结果程序被挂起了(可能是用户切换到了另外一个 app),此时连接会被断开,但是要直到程序重新唤醒时,你才知道被断开了。
利用连接 Peripheral 时的选项
Foreground-Only app 在挂起的时候,便会加入到系统的一个队列中,当程序重新唤醒时,系统便会通知程序。Core Bluetooth 会在程序中包含 central 时,给用户以提示。用户可根据提示来判断是否要唤醒该 app。
你可以利用 central 在连接 peripheral 时的方法connectPeripheral:options:中的options来触发提示:
CBConnectPeripheralOptionNotifyOnConnectionKey—— 在连接成功后,程序被挂起,�给出系统提示。
CBConnectPeripheralOptionNotifyOnDisconnectionKey—— 在程序挂起,蓝牙连接断开时,给出系统提示。
CBConnectPeripheralOptionNotifyOnNotificationKey—— 在程序挂起后,收到 peripheral 数据时,给出系统提示。
Core Bluetooth 后台模式
如果你想让你的 app 能在后台运行蓝牙,那么必须在info.plist中打开蓝牙的后台运行模式。当配置之后,收到相关事件便会从后台唤醒。这一机制对定期接收数据的 app 很有用,比如心率监测器。
下面会介绍两种后台模式,一种是作为 central 的,一种是作为 peripheral 的,如果 app 两种角色都有,那则需要开启两种模式。配置即是在info.plist中添加UIBackgroundModeskey,类型为数组,value 则根据你当前角色来选择:
bluetooth-central—— 即 Central。
bluetooth-peripheral—— 即 Peripheral。
这个配置在 Xcode 中,可以在 Capabilities 中进行配置,而不用直接面对 key-value。如果要看到 key-value,可以在info.plist中打开查看。
作为 Central 的后台模式
如果在info.plist中配置了UIBackgroundModes–bluetooth-central,那么系统则允许程序在后台处理蓝牙相关事件。在程序进入后台后,依然能扫描、搜索 peripheral,并且还能进行数据交互。当CBCentralManagerDelegate和CBPeripheralDelegate的代理方法被调用时,系统将会唤醒程序。此时允许你去处理重要的事件,比如:连接的建立或断开,peripheral 发送了数据,central manager 的状态改变。
虽然此时程序能在后台运行,但是对 peripheral 的扫描和在前台时是不一样的。实际情况是这样的:
设置的CBCentralManagerScanOptionAllowDuplicatesKey将失效,并将发现的多个 peripheral 广播的事件合并为一个。
如果全部的 app 都在后台搜索 peripheral,那么每次搜索的时间间隔会更大。这会导致搜索到 peripheral 的时间变长。
这些相应的调整会减少无线电使用,并提升续航能力。
作为 peripheral 的后台模式
作为 peripheral 时,如果需要支持后台模式,则在info.plist中配置UIBackgroundModes–bluetooth-peripheral。配置后,系统会在有读写请求和订阅事件时,唤醒程序。
在后台,除了允许处理读写请求和订阅事件外,Core Bluetooth 框架还允许 peripheral 发出广播。同样,广播事件也有前后台区别。在后台发起时是这样的:
CBAdvertisementDataLocalNameKey将失效,在广播时,广播数据将不再包含 peripheral 的名字。
被CBAdvertisementDataServiceUUIDsKey修饰的 UUID 数组将会被放到 overflow 区域中,意味着只能被明确标识了搜索 service UUID 的 iOS 设备找到。
如果所有 app 都在后台发起广播,那么发起频率会降低。
巧妙的使用后台模式
虽然程序支持一个或多个 Core Bluetooth 服务在后台运行,但也不要滥用。因为蓝牙服务会占用 iOS 设备的无线电资源,这也会间接影响到续航能力,所以尽可能少的去使用后台模式。app 会唤醒程序并处理相关事务,完成后又会快速回到挂起状态。
无论是 central 还是 peripheral,要支持后台模式都应该遵循以下几点:
程序应该提供 UI,让用户决定是否要在后台运行。
一旦程序在后台被唤醒,程序只有 10s 的时间来处理相关事务。所以应该在程序再次挂起前处理完事件。后台运行的太耗时的程序会被系统强制关闭进程。
处理无关的事件不应该唤醒程序。
和后台运行的更多介绍,可以查看App Programming Guide for iOS。
处理常驻后台任务
某些 app 可能需要 Core Bluetooth 常驻后台,比如,一款用 BLE 技术和门锁通信的 app。当用户离开时,自动上锁,回来时,自动开锁(即使程序运行在后台)。当用户离开时,可能已超出蓝牙连接范围,所以没办法给锁通信。此时可以调用CBCentralManager的connectPeripheral:options:方法,因为该方法没有超时设置,所以,在用户返回时,可以重新连接到锁。
但是还有这样的情形:用户可能离开家好几天,并且在这期间,程序已经被完全退出了。那么用户再次回家时,就不能自动开锁。对于这类 app 来说,常驻后台操作就显得尤为重要。
状态保存与恢复
因为状态的保存和恢复 Core Bluetooth 都为我们封装好了,所以我们只需要选择是否需要这个特性即可。系统会保存当前 central manager 或 peripheral manager,并且继续执行蓝牙相关事件(及时程序已经不再运行)。一旦事件执行完毕,系统会在后台重启 app,这是你有机会去存储当前状态,并且处理一些事物。在之前提到的 “门锁” 的例子中,系统会监视连接请求,并在centralManager:didConnectPeripheral:回调时,重启 app,在用户回家后,连接操作结束。
Core Bluetooth 的状态保存于恢复在设备作为 central、peripheral 或者这两种角色时,都可用。在设备作为 central 并添加了状态保存与恢复支持后,如果 app 被强行关闭进程,系统会自动保存 central manager 的状态(如果 app 有多个 central manager,你可以选择哪一个需要系统保存)。对于CBCentralManager,系统会保存以下信息:
central 需要扫描的 service(包括扫描时,配置的 options)
central 准备连接或已经连接的 peripheral
central 订阅的 characteristic
对于 peripheral 来说,情况也差不多。系统对CBPeripheralManager的处理方式如下:
peripheral 在广播的数据
peripheral 存入的 service 和 characteristic 的树形结构
已经被 central 订阅了的 characteristic 的值
当系统在后台重新加载程序后(可能是因为找到了要找的 peripheral),你可以重新实例化 central manager 或 peripheral 并恢复他们的状态。接下来会详细介绍如何存储和恢复状态。
添加状态存储和恢复支持
状态的存储和恢复功能在 Core Bluetooth 中是可选的,添加支持可以通过以下几个步骤:
(必须)在初始化 central manager 或 peripheral manager 时,要选择是否需要支持。会在文后的【选择支持存储和恢复】中介绍。
(必须)在系统从后台重新加载程序时,重新初始化 central manager 或 peripheral manager。会在文后的【重新初始化 central manager 和 peripheral manager】中介绍。
(必须)实现恢复状态相关的代理方法。会在文后的【 实现恢复状态的代理方法】中介绍。
(可选)更新 central manager 或 peripheral manager 的初始化过程。会在文后的【更新 manager 初始化过程】中介绍。
选择支持存储和恢复
如果要支持存储和恢复,则需要在初始化 manager 的时候给一个 restoration identifier。restoration identifier 是 string 类型,并标识了 app 中的 central manager 或 peripheral manager。这个 string 很重要,它将会告诉 Core Bluetooth 需要存储状态,毕竟 Core Bluetooth 恢复有 identifier 的对象。
例如,在 central 端,要想支持该特性,可以在调用CBCentralManager的初始化方法时,配置CBCentralManagerOptionRestoreIdentifierKey:
myCentralManager = [[CBCentralManager alloc] initWithDelegate:selfqueue:niloptions:@{CBCentralManagerOptionRestoreIdentifierKey :@"myCentralManagerIdentifier"}];
虽然以上代码没有展示出来,其实在 peripheral manager 中要设置 identifier 也是这样的。只是在初始化时,将 key 改成了CBPeripheralManagerOptionRestoreIdentifierKey。
因为程序可以有多个CBCentralManager和CBPeripheralManager,所以要确保每个 identifier 都是唯一的。
重新初始化 central manager 和 peripheral manager
当系统重新在后台加载程序时,首先需要做的即根据存储的 identifier,重新初始化 central manager 或 peripheral manager。如果你只有一个 manager,并且 manager 存在于 app 生命周期中,那这个步骤就不需要做什么了。
如果 app 中包含多个 manager,或者 manager 不是在整个 app 生命周期中都存在的,那 app 就必须要区分你要重新初始化哪个 manager 了。你可以通过从 app delegate 中的application:didFinishLaunchingWithOptions:中取出 key(UIApplicationLaunchOptionsBluetoothCentralsKey或UIApplicationLaunchOptionsBluetoothPeripheralsKey) 中的 value(数组类型)来得到程序退出之前存储的 manager identifier 列表:
- (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions {NSArray*centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];returnYES;}
拿到这个列表后,就可以通过循环来重新初始化所有的 manager 了。
实现恢复状态的代理方法
在重新初始化 manager 之后,接下来需要同步 Core Bluetooth 存储的他们的状态。要想弄清楚在程序被退出时都在做些什么,就需要正确的实现代理方法。对于 central manager 来说,需要实现centralManager:willRestoreState:;对于 peripheral manager 来说,需要实现peripheralManager:willRestoreState:。
注意:如果选择存储和恢复状态,当系统在后台重新加载程序时,首先调用的方法是centralManager:willRestoreState:或peripheralManager:willRestoreState:。如果没有选择存储的恢复状态(或者唤醒时没有什么内容需要恢复),那么首先调用的方法是centralManagerDidUpdateState:或peripheralManagerDidUpdateState:。
无论是以上哪种代理方法,最后一个参数都是一个包含程序退出前状态的字典。字典中,可用的 key ,central 端有:
NSString*constCBCentralManagerRestoredStatePeripheralsKey;NSString*constCBCentralManagerRestoredStateScanServicesKey;NSString*constCBCentralManagerRestoredStateScanOptionsKey;
peripheral 端有:
NSString*constCBPeripheralManagerRestoredStateServicesKey;NSString*constCBPeripheralManagerRestoredStateAdvertisementDataKey;
要恢复 central manager 的状态,可以用centralManager:willRestoreState:返回字典中的 key 来得到。假如说 central manager 有想要或者已经连接的 peripheral,那么可以通过CBCentralManagerRestoredStatePeripheralsKey对应得到的 peripheral (CBPeripheral对象)数组来得到。
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary*)state {NSArray*peripherals = state[CBCentralManagerRestoredStatePeripheralsKey];}
具体要对拿到的 peripheral 数组做什么就要根据需求来了。如果这是个 central manager 搜索到的 peripheral 数组,那就可以存储这个数组的引用,并且开始建立连接了(注意给这些 peripheral 设置代理,否则连接后不会走 peripheral 的代理方法)。
恢复 peripheral manager 的状态和 central manager 的方式类似,就只是把代理方法换成了peripheralManager:willRestoreState:,并且使用对应的 key 即可。
更新 manager 初始化过程
在实现了全部的必须步骤后,你可能想要更新 manager 的初始化过程。虽然这是个可选的操作,但是它对确保各种操作能正常进行尤为重要。假如,你的应用在 central 和 peripheral 做数据交互时,被强制退出了。即使 app 最后恢复状态时,找到了这个 peripheral,那你也不知道 central 和这个 peripheral 当时的具体状态。但其实我们在恢复时,是想恢复到程序被强制退出前的那一步。
这个需求,可以在代理方法centralManagerDidUpdateState:中,通过发现恢复的 peripheral 是否之前已经成功连接来实现:
NSUIntegerserviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj,NSUIntegerindex,BOOL*stop) {return[obj.UUID isEqual:myServiceUUIDString];}];if(serviceUUIDIndex ==NSNotFound) { [peripheral discoverServices:@[myServiceUUIDString]];}
上面的代码描述了,当系统在完成搜索 service 之后才退出的程序,可以通过调用discoverServices:方法来恢复 peripheral 的数据。如果 app 成功搜索到 service,你可以是否能搜索到需要的 characteristic(或者已经订阅过)。通过更新初始化过程,可以确保在正确的时间点,调用正确的方法。
最后
到这里,对 Core Bluetooth 的理解就暂告一段落,如果有什么问题或建议,欢迎评论。