2018年12月28日更新
这一篇是我在17年初处理BLE后台相关业务时翻译的,最初的主要目的是觉得翻译一遍能够加深自己的理解。发在这里最早是没人看的,第一条评论应该是骂我就知道抄抄抄,估计是看到了别的开发者翻译的文章了吧。最近这一篇文章的评论数飙升,所以想在这里解释下,当初为啥没放原文链接。更重要的是对大家最感兴趣的问题做个总结,结合我这一年多的开发经验,聊一聊苹果的蓝牙后台边际,说说BLE app在中心端最舒服的开发姿势。
- 1.Apple定义的后台任务是怎样的?
- 早期iOS系统无真后台,后期因为考虑到特殊场景,比如说音乐定位,后台下载等需求。苹果提供了BackGround Mode为特殊应用提供后台模式,蓝牙也在此列。
- 1.BLE Central后台可以做到什么?做不到什么?
- 通常我们实现BLE的中心时,可以通过连接外设的事件促发系统唤醒App以执行一些后台操作。但是这里的时间很短,一个很常见的Case是接受外设上传的数据并保存。永久化保存数据需要注意文件保护的坑,旧文有提过。
- 3.centralManager:willRestoreState: 和application:didFinishLaunchingWithOptions:方法
- 理论上这些方法会在外设发送数据时唤醒App,但是在新的12.X系统中发现,如果外设在App后台时断连,当系统重连外设时(绑定了ANCS),App并不能收到回调。这个有些意外。
此外App可以通过BackgroundTask来申请额外的后台时间。
最后多说一句,BLE通信技术的核心是低功耗,在电池技术迟迟不能突破的大背景下,iOS系统的基础也是以前台为王,后台的策略都是克制,广大安卓定制系统也都遵循这个套路,App的后台变成应用厂商和系统厂商之间的博弈。作为app开发,我觉得可以从业务上重新思考下产品形态,后台虽好,也不宜贪杯
以下是原文,翻译自Apple Doc.
iOS app中蓝牙的后台处理(Core Bluetooth Background Processing for iOS Apps)
对于iOS app来说,知道你的app是前台还是后台非常重要。由于iOS设备的系统资源有限,所以一个app在前台和后台时的表现一定是不一样的(iOS系统为了保证用户体验,前台应用具有资源的优先分配权,不过并不是无限的,你的app性能太差,照样会被系统收了)。关于更多的后台操作内容。请看这一章。
默认情况下,大多数常用的蓝牙任务(不论是中心还是周边的)在app后台或悬挂时都是不可用的。也就是说,你可以声明你的app支持蓝牙后台执行模式,这允许你的app在需要处理连接的那个蓝牙设备的关联事件时,系统可以把你的app从悬挂状态唤醒。即使你的app不需要全范围的后台处理支持,但是当重要的事件发生时,系统还是会通知你的app。
即使你的app实现了一种甚至两种后台执行模式的支持,它也并不是一直运行的。在某些时候,系统需要杀掉你的app来为前台app提供足够的内存资源(例如你在玩某大型手游时,内存一般会相对吃紧,这时后台应用自然就不太好过)--这会导致所有的行动和挂载的连接丢失(这里的挂载原文是pending connections,我理解是你的app和周边设备的连接,因为你注册了后台模式,所以系统为你保留了资源维持这种连接,可以使用BLE的通信,但是当内存被释放时,你的对象也就不见了)。在iOS7,Core Bluetooth框架支持保存中心管理者和周边管理者(就是你的CBCenteralManager && CBPeripheralManager 对象)的状态信息,并且支持在app 启动的时候去恢复中心管理者的状态。你可以使用这个功能去做一些蓝牙设备相关的长期行为。
前台APP(foreground-Only Apps)
除非你申请了执行指定的后台任务,大多数的app在进入后台模式之后很快就会被挂起(按下home键,大约5秒,app进入suspended状态)。当app处于挂起状态时,无法处理任何蓝牙相关的任务直到你的app进入前台(被用户唤醒)。
前台app(是指没有申请蓝牙后台模式权限的app)处于后台模式(backgroud state)或者悬挂模式(suspended state)时,在中心这边,无法搜索和发现周边在广告的设备。作为周边时,无法广告,此时中心想要通过公开服务的特征去访问它的话都会报错。
由于上述原因,你的app可能被一些默认行为影响。举个例子来说,假设你正在和你的蓝牙设备通信,这时连接是正常的,你能够得到设备给你上报的数据。想象下现在你的app被挂起了(原因可能是用户这时打开了另一个app)。如果这时和蓝牙设备的连接断了,你的app就不会收到断连的事件通知直到它再次进入前台时。
利用周边连接选项(Take Advantage of Peripheral Connection Options)
前台app在被挂起时,所以的蓝牙事件会被系统放进一个队列里,当你的app再次活跃时发给你 。这就是说,Core Bluetooth提供了一种中心角色的事件触发用户提醒机制。用户可以通过这些提示来决定要不要在某些时候把app从后台拉到前台来。
在你的终端设备调用connectPeripheral:options:方法连接远程周边设备时,你可以使用下面的这些key来设置options参数。
- CBConnectPeripheralOptionNotifyOnConnectionKey:如果想要系统在指定的周边设备在app挂起状态期间连接成功时显示一个alter提示,就使用这个key值。
- CBConnectPeripheralOptionNotifyOnDisconnectionKey:如果想要系统在指定的周边设备在app挂起状态期间断开连接时显示一个alter提示,就使用这个key值。
- CBConnectPeripheralOptionNotifyOnNotificationKey:如果想要系统在指定的周边设备在app挂起状态期间收到任何通知(这里的notifications应该是指订阅的内容)时显示一个alter提示,就使用这个key值。
想要了解更多周边连接的options,请查阅* Peripheral Connection Options*内容,地址在这。
蓝牙后台执行模式(Core Bluetooth Background Execution Modes)
如果你的app需要在后台执行一些蓝牙相关的任务,你必须在信息属性列表(Info.plist)文件中声明后台执行模式的支持。当你的app声明了它,系统会把你的app从悬挂状态唤醒以处理一些蓝牙相关事件。这个支持对app来说很重要,它可以用来和BLE设备在特定时间间隔里交付一些数据,就像个心率监控。
这里有两种模式可以供app选择,一种是作为中心角色是实现,另一种是作为周边角色。如果你的app把他两都实现了,那你就该把两种后台模式都实现了。核心蓝牙后台模式通过载Info.plist中添加UIBackgroudModes关键字到一个数组中来声明。数组中的关键包阔一下字符串:
- bluetooth-central 使用核心蓝牙框架和周边BLE设备通信
- bluetooth-peripheral 使用核心蓝牙框架共享数据
**注意:** xcode的可视界面的属性列表里,为了可读性考虑,很多key被用易读字符串替代了,如果想知道实际的key在info.plist里的名字,按住Control键并单击编辑器窗口中的任意键,并在上下文窗口中点击Show Raw Keys/Values按钮(我喜欢Open As/Source Code)。
关于怎么设置info.plist内容的详细信息,请看Xcode Help
蓝牙中心后台执行模式
当一个app在UIBackgroundModes中添加了bluetooth-central关键字在Info.plist以实现中心角色。核心蓝牙框架允许你的app在后台运行以执行一些中心蓝牙相关的任务。当你的app在后台时,你仍然可以搜索连接周边设备,和周边设备通信,交换数据。另外,系统会在任何 CBCentralManagerDelegate 或 CBPeripheralDelegate 的回调方法被调用时唤醒你的app,允许你的app处理重要的中心角色事件,比方说连接突然断了,周边角色上报数据了,中心管理者的状态发生改变。
虽然你可以在后台执行很多蓝牙相关任务,但是请记住,后台操作和app在前台始终还是不同的,尤其是你的app在后台搜索设备的时候。
- CBCentralManagerScanOptionAllowDuplicatesKey 的搜索选项将被忽略,多次扫描的结果会被合并为同一个事件
- 如果所有的app都在后台扫描,你的中心设备的搜索事件就会增长。简单说,就是你得花很长时间才能搜索到广播的设备。
这些改变帮助最小化使用广播功能以节约手机的电池电量。
蓝牙周边后台执行模式
想要作为一个周边角色在后台工作,你需要在Info.plist文件中添加bluetooth-periphral到UIBackgroundModes关键字下。当你这么做了,系统会在你的app需要读,写,订阅事件的时候唤醒它。
除了可以在后台唤醒app处理连接的中心的读写订阅。蓝牙中心库还可以允许你的app在后台的时候广播。但是你需要了解app在后台的广播和在前台的广播状态不太一样。特别的,当你的app在后台广播时。
- CBAdvertisementDataLocalNameKey 广告键是被忽略的,而且local name也不会被广播的
- 所以 CBAdvertisementDataServiceUUIDsKey中的服务UUID被放在一个“溢出”区,它们只能被明确搜索的iOS设备搜索到。
- 如果所有app都在后台广播,你的app的包广播频率会变少。
合理使用后台执行模式
虽然声明一种甚至两种蓝牙后台执行模式可以处理一些必需的场景,你也应该控制后台执行的情况。因为在后台执行时需要使用iOS设备的无线通信,这一部分对电池电量消耗比较大。在后台做尽量少的事,被任何时间唤醒的时候都应该尽快处理完然后再次进入悬挂状态。
任何app不管你声明的是哪种后台模式,都应该遵循这个使用守则。
- 必须要有用户界面可供用户开始or停止蓝牙通信
- 被唤醒时,app大概有10秒时间可以处理任务,理想状况下,它最好能在10秒内处理完,然后再次被挂起。app在后台逗留太长时间会被系统限制甚至杀死。
- app的后台模式不应该被用来处理非蓝牙相关任务
更多关于app后台行为的信息,请看资料Being a Responsible Background App章节。
后台执行较长时间任务
有一些app可能需要在后台较长时间使用核心蓝牙模块。举个例子,你可能需要开发一款家居安全的app在iOS设备上,它可以和你的门锁(实现了BLE协议的)通信。这个app可以在用户离开家的时候自动锁门,用户回家时自动开门-所有这些情况app都在后台。当用户离开家,iOS设备会超出门锁的通信范围,连接就会断开。这时,app只需调用CBCentralManager类的connectPeripheral:options:方法,而且由于连接请求不会超时,当用户回家时,设备将重新连接,门锁将会打开。
现在想象下用户经常会离开家好几天。如果你的app在用户离开家时就被terminated,这个app就不能在用户回家时重新连上这个锁了,用户就进不了门啦。像这样的app,被允许执行长时间的行为,像监控行为和保持连接。
状态的保存和恢复
由于状态的保存和恢复是内置在CoreBluetooth框架之中的,你的app可以选择这个功能,请求系统保存app的中心或者周边管理者的状态,用来在某些行为下执行一些蓝牙相关任务,即使你的app已经不在运行了。当一个这样的任务完成时,系统重启你的app到后台状态并且给它一个机会保存它的状态以处理适当的事件。像上面提到的那个家居安全的app,当用户回家连接请求完成时,系统会管理连接请求,重启app并调用委托的回调函数centralManager:didConnectPeripheral:。
CoreBluetooth支持app实现中心或者周边角色的状态保存和恢复,一起实现也可以。当你的app实现中心角色并且添加支持了状态的保存和恢复,当系统计划终止你的app以释放资源时它会保存你的中心管理者对象的状态(如果你的app有多个中心管理者,你可以选择你希望系统保存的那个)。特别的,系统会为中心管理者跟踪以下信息:
- 中心管理者已搜索到的服务(任何搜索指定选项开始之后的)
- 所以中心管理者准备连接或者已经连接的周边
- 中心管理者已经订阅的特征
实现周边角色的app也可以利用状态的保存和恢复,系统为周边管理者跟踪以下信息:
- 周边正在广告的数据
- 周边管理者公开在设备数据库的服务和特征
- 已经订阅某些特征数据的中心
当你的app在后台被系统重启(比方说你的app搜索的那个周边设备被发现了),你可以重新启动应用程序的中央和周边管理器并恢复其状态。接下来的小节详细讨论怎样在你的app中使用状态恢复与保存
添加状态的保存和恢复
状态的恢复与保存是CoreBluetooth的一个可选功能,添加它需要你的app做一些工作,你可以通过下面步骤做到:
- 1.(必须的)在初始化中心或者周边管理者的时候选择是否需要支持状态的保存和恢复,这个步骤在“选择状态的保存和恢复”部分有详细介绍。
- 2.(必须的)在系统重启app时恢复你的中心或周边角色,这个步骤的详情在“恢复你的中心和周边管理者”部分。
- 3.(必须的)实现适当的保存委托方法。这个步骤在“实现合适的保存委托方法”中有介绍。
- 4.(可选的)更新你的中心和周边管理者的初始化过程。这个步骤在“更新的你初始化过程”中有介绍。
选择状态的保存和恢复
在app中加入状态的保存和恢复功能的方式很简单,只需要在初始化中心|周边管理者的时候提供一个唯一的恢复字符串。一个“恢复标识”就是一个为Corebluetooth和你的app标记中心|周边管理者的字符串。这个字符串的值只有你的代码知道,但是这个字符串的存在高速CoreBluetooth它需要保存这个被标记对象的状态。CoreBluetooth也仅仅会保存这些有“恢复标示”的对象。
举例来说,对于一个只使用一个CBCentralManager实例对象以实现中心角色的app来说,想要添加状态的保存和恢复功能,只要在初始化时指定CBCentralManagerOptionRestoreIdentifierKey选项,并为中心管理者提供一个字符串作为“恢复标识”就可以了:
myCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:@{ CBCentralManagerOptionRestoreIdentifierKey: @"myCentralManagerIdentifier" }];
虽然上面的例子并没有指出,但是在app里实现周边管理者的保存和恢复也是类似的操作,实现选项的关键字是:CBPeripheralManagerOptionRestoreIdentifierKey,初始化时提供一个字符串作为“恢复标识”就好。
注意:由于一个app可以拥有多个周边|中心管理者的实例,一定要保证“恢复标识”的唯一性,这样系统才能在需要的时候区分开来
恢复你的中心和周边管理者
当你的app在后台被系统重启时,你的第一件事就是根据“恢复标识”恢复适当的中心和周边管理者就像他们第一次创建时一样。如果你的app只使用了一个中心|周边管理者,并且这个管理者的生命周期和你的app差不多长,这里你就不需要做其他事了。
如果你的app使用了不止一个中心|周边管理者,又或者app使用的管理者生命周期没有app那么长,你的app需要知道哪个管理者在系统重启app时被恢复了。在实现你的app委托方法application:didFinishLaunchingWithOptions:时使用适当的启动选项键(UIApplicationLaunchOptionsBluetoothCentralsKey||UIApplicationLaunchOptionsBluetoothPeripheralsKey),你可以访问系统在终止应用程序时为其保留的管理对象的所有恢复标识符列表。
举个例子,当你的app被系统重启时,你可以检索系统为你的应用程序保留的中央管理器对象的所有恢复标识符,像这样:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey]; ...
在拿到恢复标示符之后,只需要遍历并恢复适当的中央管理者。
注意:在你的app重启时,系统仅为正在执行某些蓝牙相关任务(而该应用不再运行)中央|周边管理器提供恢复标识符。这些启动选项键在UIApplicationDelegate协议相关中有详细介绍.
实现合适的保存委托方法
在你的应用程序中重新配置适当的中央和外围管理器后,通过将其状态与蓝牙系统的状态同步来恢复它们。为了使你的应用程序能够以系统所代表的方式(当它不运行时)加快速度,你必须实现相应的恢复委托方法。对于中心管理者,实现centralManager:willRestoreState:方法;对于周边管理者,实现peripheralManager:willRestoreState:方法
重要:对于一个实现了蓝牙状态保存和恢复功能选项的app而言,这些方法是在被后台重启以执行蓝牙相关任务是最先被调用的:(centralManager:willRestoreState: 和periphetalManager:willRestoreState: ),对于不选择进行状态保存的应用程序(或者如果启动时没有恢复任何内容),则首先调用centralManagerDidUpdateState:和peripheralManagerDidUpdateState:方法。
上面的两种委托方法,最后一个参数都是一个包含管理者在app上一次被终止时保存的信息的字典。有关字典可用键的列表,Central Manager State Restoration Options相关常量在CBCentralManagerDelegate Protocol Reference 中,Peripheral_Manager_State_Restoration_Options相关常量在CBPeripheralManagerDelegate Protocol Reference.
为了恢复CBCentralManager对象的状态,使用centralManager:willRestoreState:方法提供的参数字典中包含的键。举例来说,如果你的中心管理者对象在上一次被终止时有任何行为或者没完成的连接,系统会代表你的app继续监控完成它。就像下面这样,你可以使用CBCentralManagerRestoredStatePeripheralsKey字典关键字以得到所有这个中心已连接或者想要连接的周边的列表:
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary *)state { NSArray *peripherals = state[CBCentralManagerRestoredStatePeripheralsKey]; ...
在上述示例中,使用已恢复的外围设备列表进行的操作取决于用例。例如,如果您的应用程序保留了中央管理员发现的外围设备列表,则可能需要将恢复的外围设备添加到该列表中以保持对其的引用。就像Connecting to a Peripheral Device After You’ve Discovered It小节所描述的,设置一个外设代理,以确保它接收到相应的回调。
您可以通过使用外设管理器在CBPeripheralManager:willRestoreState:方法中提供的字典的键,以类似的方式恢复CBPeripheralManager对象。
更新的你初始化过程
在实施了前面三个必须的步骤之后,您可能需要查看更新中央和外围管理器的初始化过程。通过这个可选步骤,确保您的应用程序运行顺利,这一点很重要。例如,你的应用程序在探索连接的周边设备的数据的过程中可能已被终止。当你的应用程序使用此外设进行恢复时,它将不知道它在终止时的发现过程有多远。您将需要确保您从发现过程中离开的地方开始。
例如,在centralManagerDidUpdateState:委托方法中初始化您的应用程序时,您可以了解您是否已成功发现已还原外设的特定服务(在应用程序终止之前),如下所示:
NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj, NSUInteger index, BOOL *stop) { return [obj.UUID isEqual:myServiceUUIDString]; }]; if (serviceUUIDIndex == NSNotFound) { [peripheral discoverServices:@[myServiceUUIDString]]; ...
像上面例子所示:如果系统在完成发现服务之前终止了应用程序,请通过调用discoverServices:开始探索恢复的外围设备的数据。 如果你的应用程序成功发现服务,你可以检查是否发现了相应的特征(以及是否已订阅它们)。 通过以这种方式更新初始化过程,你将确保在正确的时间调用正确的方法。