『CoreBluetooth』8. 后台运行蓝牙服务

这是 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 中添加 UIBackgroundModes key,类型为数组,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 中是可选的,添加支持可以通过以下几个步骤:

  1. (必须)在初始化 central manager 或 peripheral manager 时,要选择是否需要支持。会在文后的【选择支持存储和恢复】中介绍。
  2. (必须)在系统从后台重新加载程序时,重新初始化 central manager 或 peripheral manager。会在文后的【重新初始化 central manager 和 peripheral manager】中介绍。
  3. (必须)实现恢复状态相关的代理方法。会在文后的【 实现恢复状态的代理方法】中介绍。
  4. (可选)更新 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:self queue:nil
         options:@{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 *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    NSArray *centralManagerIdentifiers = launchOptions[UIApplicationLaunchOptionsBluetoothCentralsKey];
    return YES;
}

拿到这个列表后,就可以通过循环来重新初始化所有的 manager 了。

实现恢复状态的代理方法

在重新初始化 manager 之后,接下来需要同步 Core Bluetooth 存储的他们的状态。要想弄清楚在程序被退出时都在做些什么,就需要正确的实现代理方法。对于 central manager 来说,需要实现 centralManager:willRestoreState:;对于 peripheral manager 来说,需要实现 peripheralManager:willRestoreState:

注意:如果选择存储和恢复状态,当系统在后台重新加载程序时,首先调用的方法是 centralManager:willRestoreState: 或 peripheralManager:willRestoreState:。如果没有选择存储的恢复状态(或者唤醒时没有什么内容需要恢复),那么首先调用的方法是 centralManagerDidUpdateState: 或 peripheralManagerDidUpdateState:

无论是以上哪种代理方法,最后一个参数都是一个包含程序退出前状态的字典。字典中,可用的 key ,central 端有:

NSString *const CBCentralManagerRestoredStatePeripheralsKey;
NSString *const CBCentralManagerRestoredStateScanServicesKey;
NSString *const CBCentralManagerRestoredStateScanOptionsKey;

peripheral 端有:

NSString *const CBPeripheralManagerRestoredStateServicesKey;
NSString *const CBPeripheralManagerRestoredStateAdvertisementDataKey;

要恢复 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 是否之前已经成功连接来实现:

NSUInteger serviceUUIDIndex = [peripheral.services indexOfObjectPassingTest:^BOOL(CBService *obj, NSUInteger index, BOOL *stop) {
    return [obj.UUID isEqual:myServiceUUIDString];
}];

if (serviceUUIDIndex == NSNotFound) {
    [peripheral discoverServices:@[myServiceUUIDString]];
}

上面的代码描述了,当系统在完成搜索 service 之后才退出的程序,可以通过调用 discoverServices: 方法来恢复 peripheral 的数据。如果 app 成功搜索到 service,你可以是否能搜索到需要的 characteristic(或者已经订阅过)。通过更新初始化过程,可以确保在正确的时间点,调用正确的方法。

最后

到这里,对 Core Bluetooth 的理解就暂告一段落,如果有什么问题或建议,欢迎评论。

《『CoreBluetooth』8. 后台运行蓝牙服务》有48个想法

          1. 后台常驻模式开发时 应用被kill了之后连接不上 但是如果没有被kill的情况下 比如说我晚上下班打开app 第二天早上是会自动连接的 所以这些操作要写在appdelegate里吗

          2. 楼主,这个能实现APP长后台吗?即在后台运行4-5天不被杀死。我试了你的方法,能恢复APP能连接上蓝牙。但是不能收发数据了。好像是APP已经挂了

  1. 请问楼主:
    1.”peripheralManager:willRestoreState: “这些方法是写到Appdelegate.m这个类中?
    2.最后[更新 manager 初始化过程]一节中,”但其实我们在恢复时,是想恢复到程序被强制退出前的那一步。通过发现恢复的 peripheral 是否之前已经成功连接来实现:” 这句话的意思,我们能够知道程序退出之前的peripheral的状态,这个状态只是指的我们可以知道”未和central连接成功”和”已经和central连接成功”这两种吗?

    1. ”peripheralManager:willRestoreState: “ 这个CBPeripheralManager的代理方法,是怎么样才能够执行,你知道吗?可以加我的QQ吗?2295323316,谢谢了。

  2. 博主您好,请问您的这篇文章的原文怎么找不到了?http://ju.outofmemory.cn/entry/213827,是内容变了还是怎么样?因为有个课程需要讲到GCD死锁,感觉您的案例讲得非常清晰,希望可以授权使用。

  3. 楼主,demo跑起来崩了,
    原因:崩在了这个方法里面,是不是要吧ServiceUUIDString1、ServiceUUIDString2换成自己的?
    – (NSArray *)serviceUUIDArray {
    if (!_serviceUUIDArray) {
    CBUUID *serviceUUID1 = [CBUUID UUIDWithString:ServiceUUIDString1];
    CBUUID *serviceUUID2 = [CBUUID UUIDWithString:ServiceUUIDString2];
    _serviceUUIDArray = @[serviceUUID1, serviceUUID2];
    }
    return _serviceUUIDArray;
    }
    2016-07-18 15:03:49.965 STBLETool[4386:2499446] *** Assertion failure in -[CBUUID initWithString:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/CoreBluetooth/CoreBluetooth-327.1/

  4. 请教博主,蓝牙后台模式,我要如何让蓝牙后台常驻后,一直能被动接收设备端定时发送的蓝牙广播包,手机端是central 设备端是peripheral。我要实现的是手机后台模式时,靠近设备收到广播包唤醒手机蓝牙,app应用没有后台功能。

  5. 我有个疑惑,能否让app能在后台一直运行,蓝牙可以持续扫描,连接,通讯,而不是只是10秒钟的时间。请解答下!如果不能实现这种功能,最后赋上苹果官方的说明,我们需要及时调整开发的方案。谢谢

  6. 楼主好厉害。求教 app 退出后怎么接收蓝牙命令?手机是 central,app 在后台时接收没问题,但是一旦被 kill 就没发接收到蓝牙信息,可以让 app 接收到蓝牙信息后重启吗?手机和外设的蓝牙一直是连上的。

  7. 想问楼主有没有蓝牙恢复与保存的demo,一直没研究出来,蓝牙后台唤醒,现在是通过无声音乐实现app后台运行的

  8. 向楼主请教一下,蓝牙的常驻后台任务,就像例子里说的,我要是离家好几天,甚至APP被杀死了,要怎么唤醒APP呢
    UIApplicationLaunchOptionsBluetoothCentralsKey
    UIApplicationLaunchOptionsBluetoothPeripheralsKey
    这两个key在什么情况下会触发(即在什么样的情况下APP可以因为蓝牙被唤醒)

    1. 你好,关于这连个key,你找到使用它们的方法了吗?我怎么试都不行,你知道是为什么吗?方便加我的QQ吗? 2295323316,谢谢了

  9. 楼主你好,请问一下,我现在有个需求是手机作为外设,然后想后台发送iBeacon的信号,是跟蓝牙一样的吗?

      1. 首先感谢你的博客教会我,很多东西.公司最近在做玩具,需要蓝牙这方面,我准备封装一个和你demo上一样的工具类,不过要求是swift,我对swift不熟悉,特意来请教一下怎么整

  10. 请问我后台蓝牙为什么锁屏/黑屏情况下不能收发消息,
    但是只要手机屏幕是亮的,App在后台的话是可以一直获取的。
    锁屏。黑屏的情况和app在后台的情况有什么不同吗?感觉是10.3才出现的问题。

  11. 群主,看了群主的博客,觉得不错。现在正在做一个功能,就是后台持续扫描外设并操作设备。杀死app这样的操作还能继续不?

  12. 楼主请问我的APP已经能在后台工作
    但是有一只手机 i6 10.3.3
    当按POWER键进入 sleep mode 后,
    按蓝牙app会执行,但萤幕保护不会被唤醒
    也就是不会出现萤幕保护图片,程式的执行速度会大幅降低

    不准楼主是否知道原因或解决方式?

发表评论

电子邮件地址不会被公开。