5、外设模式的使用
5.1 App 作为外设被连接的实现
1、启动一个 Peripheral 管理对象
打开 peripheralManager,设置 peripheralManager 的委托。
2、配置本地 Peripheral,设置服务、特性、描述、权限等等
创建 characteristics,characteristics 的 description,创建 service,把 characteristics 添加到 service 中,再把 service 添加到 peripheralManager 中。
当 peripheral 成功打开后,才可以配置 service 和 characteristics。这里创建的 service 和 characteristics 对象是 CBMutableCharacteristic 和 CBMutableService。他们的区别就像 NSArray 和 NSMutableArray 区别类似。我们先创建 characteristics 和 description,description 是 characteristics 的描述,描述分很多种,常用的就是 CBUUIDCharacteristicUserDescriptionString。
3、开启广播 advertising
添加发送广播后悔调用代理的 peripheralManagerDidStartAdvertising:error: 方法。
4、设置处理订阅、取消订阅、读 characteristic、写 characteristic 的委托方法
5.2 作为 Peripheral 时的请求响应
5.2.1 初始化 CBPeripheralManager
将设备作为 peripheral,第一步就是初始化 CBPeripheralManager 对象。可以通过调用 CBPeripheralManager 的initWithDelegate:queue:options:方法来进行初始化:
上面的几个参数中,将 self 设为代理来接收相关回调,queue 为 nil 表示在主线程。
当你调用上面这方法后,便会回调peripheralManagerDidUpdateState:。所以在此之前,你需要先遵循CBPeripheralManagerDelegate。这个代理方法能获取当前 iOS 设备能否作为 peripheral。
5.2.2 配置 service 和 characteristic
就像之前讲到的一样,peripheral 数据库是一个树形结构。
Bluetooth24
所以在创建 peripheral 的时候,也要像这种树形结构一样,将 service 和 characteristic 装进去。在此之前,我们需要做的是学会如何标识 service 和 characteristic。
1、使用 UUID 来标识 service 和 characteristic
service 和 characteristic 都通过 128 位的 UUID 来进行标识,Core Bluetooth 将 UUID 封装为了 CBUUID 。关于详细 UUID 的介绍,请参考上面的 4.3.1 CBUUID 讲解。
2、为自定义的 service 和 characteristic 创建 UUID
你的 service 或者 characteristic 的 UUID 并没有公共的 UUID,这时你需要创建自己的 UUID。
使用命令行的uuidgen能很容易的生成 UUID。首先打开终端,为你的每一个 service 和 characteristic 创建 UUID。在终端输入uuidgen然后回车,具体如下:
可以通过UUIDWithString:方法,将 UUID 生成 CBUUID 对象。
3、构建 service 和 characteristic 树形结构
在将 UUID 打包为 CBUUID 之后,就可以创建 CBMutableService 和 CBMutableCharacteristic 并把他们组成一个树形结构了。创建 CBMutableCharacteristic 对象可以通过该类的initWithType:properties:value:permissions:方法:
创建 characteristic 的时候,就为他设置了 properties 和 permissions。这两个属性分别定义了 characteristic 的可读写状态和 central 连接后是否能订阅。上面这种初始化方式,代表着 characteristic 可读。更多的选项,可以去看看CBMutableCharacteristic Class Reference。
如果给 characteristic 设置了 value 参数,那么这个 value 会被缓存,并且 properties 和 permissions 会自动设置为可读。如果想要 characteristic 可写,或者在其生命周期会改变它的值,那需要将 value 设置为 nil。这样的话,就会动态的来处理 value 。
现在已经成功的创建了 characteristic,下一步就是创建一个 service,并将它们构成树形结构。调用 CBMutableService 的initWithType:primary:方法来初始化 service:
第二个参数 primary 设置为 YES 表示该 service 为 primary service(主服务),与 secondary service(次服务)相对。primary service 描述了设备的主要功能,并且能包含其他 service。secondary service 描述的是引用它的那个 service 的相关信息。比如,一个心率监测器,primary service 描述的是当前心率数据,secondary service 描述描述的是当前电量。
创建了 service 之后,就可以包含 characteristic 了:
5.2.3 发布 service 和 characteristic
构建好树形结构之后,接下来便需要将这结构加入设备的数据库。这一操作 Core Bluetooth 已经封装好了,调用 CBPeripheralManager 的addService:方法即可:
当调用以上方法时,便会回调 CBPeripheralDelegate 的peripheralManager:didAddService:error:回调。当有错误,或者当前 service 不能发布的时候,可以在这个代理中来进行检测:
当你发布 service 之后,service 就会缓存下来,并且无法再修改。
5.2.4 广播 service
搞定发布 service 和 characteristic 之后,就可以开始给正在监听的 central 发广播了。可以通过调用 CBPeripheralManager 的startAdvertising:方法并传入字典作为参数来进行广播:
上面的代码中,key 只用到了 CBAdvertisementDataServiceUUIDsKey,对应的 value 是包含需要广播的 service 的 CBUUID 类型数组。除此之外,还有以下 key:
但是只有 CBAdvertisementDataLocalNameKey 和 CBAdvertisementDataServiceUUIDsKey 才是 peripheral Manager 支持的。
当开始广播时,peripheral Manager 会回调peripheralManagerDidStartAdvertising:error:方法。如果有错或者 service 无法进行广播,则可以在该该方法中检测:
因为空间的限制,并且还可能有多个 app 在同时发起广播,所以数据广播基于 best effort(即在接口发生拥塞时,立即丢包,直到业务量减小)。
广播服务在程序挂起时依然可用。
5.2.5 响应 central 的读写操作
在连接到一个或多个 central 之后,peripheral 有可能会收到读写请求。此时,你应该根据请求作出相应的响应,接下来便会提到这方面的处理。
1、读取请求
当收到读请求时,会回调peripheralManager:didReceiveReadRequest:方法。该回调将请求封装为了 CBATTRequest 对象,在该对象中,包含很多可用的属性。
其中一种用法是在收到读请求时,可以通过 CBATTRequest 的 characteristic 属性来判断当前被读的 characteristic 是哪一个 characteristic:
匹配上 UUID 之后,接下来需要确保读取数据的 offset(偏移量)不会超过 characteristic 数据的总长度:
假设偏移量验证通过,下面需要截取 characteristic 中的数据,并赋值给request.value。注意,offset 也要参与计算:
读取完成后,记着调用 CBPeripheralManager 的respondToRequest:withResult:方法,告诉 central 已经读取成功了:
如果 UUID 匹配不上,或者是因为其他原因导致读取失败,那么也应该调用respondToRequest:withResult:方法,并返回失败原因。官方提供了一个失败原因枚举,可能有你需要的。
2、写入请求
写入请求和读取请求一样简单。当 central 想要写入一个或多个 characteristic 时,CBPeripheralManager 回调peripheralManager:didReceiveWriteRequests:。该方法会获得一个 CBATTRequest 数组,包含所有写入请求。当确保一切验证没问题后(与读取操作验证类似:UUID 与 offset),便可以进行写入:
成功后,同样去调用respondToRequest:withResult:。但是和读取操作不同的是,读取只有一个 CBATTRequest,但是写入是一个 CBATTRequest 数组,所以这里直接传入第一个 request 就行:
因为收到的是一个请求数组,所以,当他们其中有任何一个不满足条件,那就不必再处理下去了,直接调用respondToRequest:withResult:方法返回相应的错误。
5.2.6 发送更新数据给订阅了的 central
central 可能会订阅了一个或多个 characteristic,当数据更新时,需要给他们发送通知。下面就来详细介绍下。
当 central 订阅 characteristic 的时候,会回调 CBPeripheralManager 的peripheralManager:central:didSubscribeToCharacteristic:方法:
通过上面这个代理,可以用个数组来保存被订阅的 characteristic,并在它们的数据更新时,调用 CBPeripheralManager 的updateValue:forCharacteristic:onSubscribedCentrals:方法来告诉 central 有新的数据:
这个方法的最后一个参数能指定要通知的 central。如果参数为 nil,则表示想所有订阅了的 central 发送通知。
同时updateValue:forCharacteristic:onSubscribedCentrals:方法会返回一个 BOOL 标识是否发送成功。如果发送队列任务是满的,则会返回 NO。当有可用的空间时,会回调peripheralManagerIsReadyToUpdateSubscribers:方法。所以你可以在这个回调用调用updateValue:forCharacteristic:onSubscribedCentrals:重新发送数据。
发送数据使用到的是通知,当你更新订阅的 central 时,应该调用一次updateValue:forCharacteristic:onSubscribedCentrals:。
因为 characteristic 数据大小的关系,不是所有的更新都能发送成功,这种问题应该由 central 端来处理。调用 CBPeripheral 的readValueForCharacteristic:方法,来主动获取数据。
5.3 请求响应 - 最佳实践
5.3.1 关于广播的思考
广播是 peripheral 的一个重要操作,接下来会讲到广播的正确姿势。
1、注意广播对数据大小的限制
正如前文提到过的那样,广播是通过调用 CBPeripheralManager 的startAdvertising:方法发起的。当你将要发送的数据打包成字典后,千万要记住数据大小是有限制的。
即使广播可以包含 peripheral 的很多信息,但是其实只需要广播 peripheral 的名称和 service 的 UUID 就足够了。也就是构建字典时,填写 CBAdvertisementDataLocalNameKey 和 CBAdvertisementDataServiceUUIDsKey 对应的 value 即可,如果使用其他 key,将会导致错误。
当 app 运行在前台时,有 28 bytes 的空间可用于广播。如果这 28 bytes 用完了,则会在扫描响应时额外分配 10 bytes 的空间,但这空间只能用于被 CBAdvertisementDataLocalNameKey 修饰的 local name(即在startAdvertising:时传入的数据)。以上提到的空间,均不包含 2 bytes 的报文头。被 CBAdvertisementDataServiceUUIDsKey 修饰的 service 的 UUID 数组数据,均不会添加到特殊的 overflow 区域。并且这些 service 只能被 iOS 设备发现。当程序挂起后,local name 和 UUID 都会被加入到 overflow 区。
为了保证在有限的空间中,正确的标识设备和 service UUID,请正确构建广播的数据。
2、只广播必要的数据
当 peripheral 想要被发现时,它会向外界发送广播,此时会用到设备的无线电(当然还有电池)。一旦连接成功,central 便能直接从 peripheral 中读取数据了,那么此时广播的数据将不再有用。所以,为了减少无线电的使用、提高手机性能、保护设备电池,应该在被连接后,及时关闭广播。停止广播调用 CBPeripheralManager 的stopAdvertising方法即可。
3、手动开启广播
其实什么时候应该广播,多数情况下,用户比我们更清楚。比如,他们知道周围没有开着的 BLE 设备,那他就不会把 peripheral 的广播打开。所以提供给用户一个手动开启广播的 UI 更为合适。
5.3.2 配置 characteristic
在创建 characteristic 的时候,就为它设定了相应的 properties、value 和 promissions。这些属性决定了 central 如何和 characteristic 通信。properties 和 promissions 可能需要根据 app 的需求来设置,下来就来谈谈如何配置 characteristic:
1、让 characteristic 支持通知
之前在 central 的时候提到过,如果要读取经常变化的 characteristic 的数据,更推荐使用订阅。所以,如果可以,最好 characteristic 允许订阅。
如果像下面这样初始化 characteristic 就是允许读和订阅:
2、限制只能配对的 central 才能访问敏感信息
有些时候,可能有这样的需求:需要 service 的一个或多个 characteristic 的数据安全性。假如有一个社交媒体的 service,那么它的 characteristic 可能包含了用户的姓名、邮箱等私人信息,所以只让信任的 central 才能访问这些数据是很有必要的。
这可以通过设置相应的 properties 和 promissions 来达到效果:
像上面这样设置,便能只让配对的 central 才能进行订阅。并且在连接过程中,Core Bluetooth 还会自动建立安全连接。
在尝试配对时,两端都会弹出警告框,central 端会提供 code,peripheral 端必须要输入该 code 才能配对成功。成功之后,peripheral 才会信任该 central,并允许读写数据。
6、后台运行蓝牙服务
对于 iOS app 来说,知道现在是运行在前台和后台是至关重要的。因为当程序挂起后,对资源的使用是相当有限的。关于多任务的介绍,可以看app 开发手册。
默认情况下,Core Bluetooth 是不会在后台运行的(无论是 central 还是 peripheral)。但你也可以配置在 app 收到事件后,从挂起状态唤醒。即使程序不是完全的支持后台模式,也可以要求在有重要事件时接收系统通知。
即使在以上两种情况下(完全允许后台和部分允许后台),程序也有可能不会永远挂起。在前台程序需要更多内存时,被挂起的程序很有可能会被强制退出,那样会断开所有的连接。从 iOS 7 开始,能够先保存状态(无论是 central 还是 peripheral),并在重新打开 app 时还原这些状态。通过这一特性,就可以做长时间操作了。
6.1 运行在前台的 app(Foreground-Only)
除非去申请后台权限,否则 app 都是只在前台运行的,程序在进入后台不久便会切换到挂起状态。挂起后,程序将无法再接收任何蓝牙事件。
对于 central 来说,挂起将无法再进行扫描和搜索 peripheral。对于 peripheral 来说,将无法再发起广播,central 也无法再访问动态变化的 characteristic 数据,访问将返回 error。
根据不同情况,这种机制会影响程序在以下几个方面的运用。你正在读取 peripheral 的数据,结果程序被挂起了(可能是用户切换到了另外一个 app),此时连接会被断开,但是要直到程序重新唤醒时,你才知道被断开了。
1、利用连接 Peripheral 时的选项
Foreground-Only app 在挂起的时候,便会加入到系统的一个队列中,当程序重新唤醒时,系统便会通知程序。Core Bluetooth 会在程序中包含 central 时,给用户以提示。用户可根据提示来判断是否要唤醒该 app。
可以利用 central 在连接 peripheral 时的方法connectPeripheral:options:中的 options 来触发提示:
6.2 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 中打开查看。
1、作为 Central 的后台模式
如果在 info.plist 中配置了 UIBackgroundModes – bluetooth-central,那么系统则允许程序在后台处理蓝牙相关事件。在程序进入后台后,依然能扫描、搜索 peripheral,并且还能进行数据交互。当 CBCentralManagerDelegate 和 CBPeripheralDelegate 的代理方法被调用时,系统将会唤醒程序。此时允许你去处理重要的事件,比如:连接的建立或断开,peripheral 发送了数据,central manager 的状态改变。
虽然此时程序能在后台运行,但是对 peripheral 的扫描和在前台时是不一样的。实际情况是这样的:
设置的 CBCentralManagerScanOptionAllowDuplicatesKey 将失效,并将发现的多个 peripheral 广播的事件合并为一个。
如果全部的 app 都在后台搜索 peripheral,那么每次搜索的时间间隔会更大。这会导致搜索到 peripheral 的时间变长。
这些相应的调整会减少无线电使用,并提升续航能力。
2、作为 peripheral 的后台模式
作为 peripheral 时,如果需要支持后台模式,则在 info.plist 中配置 UIBackgroundModes – bluetooth-peripheral。配置后,系统会在有读写请求和订阅事件时,唤醒程序。
在后台,除了允许处理读写请求和订阅事件外,Core Bluetooth 框架还允许 peripheral 发出广播。同样,广播事件也有前后台区别。在后台发起时是这样的:
CBAdvertisementDataLocalNameKey 将失效,在广播时,广播数据将不再包含 peripheral 的名字。
被 CBAdvertisementDataServiceUUIDsKey 修饰的 UUID 数组将会被放到 overflow 区域中,意味着只能被明确标识了搜索 service UUID 的 iOS 设备找到。
如果所有 app 都在后台发起广播,那么发起频率会降低。
6.3 巧妙的使用后台模式
虽然程序支持一个或多个 Core Bluetooth 服务在后台运行,但也不要滥用。因为蓝牙服务会占用 iOS 设备的无线电资源,这也会间接影响到续航能力,所以尽可能少的去使用后台模式。app 会唤醒程序并处理相关事务,完成后又会快速回到挂起状态。
无论是 central 还是 peripheral,要支持后台模式都应该遵循以下几点:
程序应该提供 UI,让用户决定是否要在后台运行。
一旦程序在后台被唤醒,程序只有 10s 的时间来处理相关事务。所以应该在程序再次挂起前处理完事件。后台运行的太耗时的程序会被系统强制关闭进程。
处理无关的事件不应该唤醒程序。
和后台运行的更多介绍,可以查看App Programming Guide for iOS。
6.4 处理常驻后台任务
某些 app 可能需要 Core Bluetooth 常驻后台,比如,一款用 BLE 技术和门锁通信的 app。当用户离开时,自动上锁,回来时,自动开锁(即使程序运行在后台)。当用户离开时,可能已超出蓝牙连接范围,所以没办法给锁通信。此时可以调用 CBCentralManager 的connectPeripheral:options:方法,因为该方法没有超时设置,所以,在用户返回时,可以重新连接到锁。
但是还有这样的情形:用户可能离开家好几天,并且在这期间,程序已经被完全退出了。那么用户再次回家时,就不能自动开锁。对于这类 app 来说,常驻后台操作就显得尤为重要。
1、状态保存与恢复
因为状态的保存和恢复 Core Bluetooth 都为我们封装好了,所以我们只需要选择是否需要这个特性即可。系统会保存当前 central manager 或 peripheral manager,并且继续执行蓝牙相关事件(即使程序已经不再运行)。一旦事件执行完毕,系统会在后台重启 app,这时你有机会去存储当前状态,并且处理一些事物。在之前提到的 “门锁” 的例子中,系统会监视连接请求,并在centralManager:didConnectPeripheral:回调时,重启 app,在用户回家后,连接操作结束。
Core Bluetooth 的状态保存与恢复在设备作为 central、peripheral 或者这两种角色时,都可用。在设备作为 central 并添加了状态保存与恢复支持后,如果 app 被强行关闭进程,系统会自动保存 central manager 的状态(如果 app 有多个 central manager,你可以选择哪一个需要系统保存)。
对于 CBCentralManager,系统会保存以下信息:
central 准备连接或已经连接的 peripheral
central 需要扫描的 service(包括扫描时,配置的 options)
central 订阅的 characteristic
对于 peripheral 来说,情况也差不多。系统对 CBPeripheralManager 的处理方式如下:
peripheral 在广播的数据
peripheral 存入的 service 和 characteristic 的树形结构
已经被 central 订阅了的 characteristic 的值
当系统在后台重新加载程序后(可能是因为找到了要找的 peripheral),你可以重新实例化 central manager 或 peripheral 并恢复他们的状态。
2、添加状态存储和恢复支持
状态的存储和恢复功能在 Core Bluetooth 中是可选的,添加支持可以通过以下几个步骤:
(必须)在初始化 central manager 或 peripheral manager 时,要选择是否需要支持。会在文后的【3、选择支持存储和恢复】中介绍。
(必须)在系统从后台重新加载程序时,重新初始化 central manager 或 peripheral manager。会在文后的【4、重新初始化 central manager 和 peripheral manager】中介绍。
(必须)实现恢复状态相关的代理方法。会在文后的【5、实现恢复状态的代理方法】中介绍。
(可选)更新 central manager 或 peripheral manager 的初始化过程。会在文后的【6、更新 manager 初始化过程】中介绍。
3、选择支持存储和恢复
如果要支持存储和恢复,则需要在初始化 manager 的时候给一个 restoration identifier。restoration identifier 是 string 类型,并标识了 app 中的 central manager 或 peripheral manager。这个 string 很重要,它将会告诉 Core Bluetooth 需要存储状态,毕竟 Core Bluetooth 恢复有 identifier 的对象。
例如,在 central 端,要想支持该特性,可以在调用 CBCentralManager 的初始化方法时,配置 CBCentralManagerOptionRestoreIdentifierKey:
虽然以上代码没有展示出来,其实在 peripheral manager 中要设置 identifier 也是这样的。只是在初始化时,将 key 改成了 CBPeripheralManagerOptionRestoreIdentifierKey。
因为程序可以有多个 CBCentralManager 和 CBPeripheralManager,所以要确保每个 identifier 都是唯一的。
4、重新初始化 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 列表:
拿到这个列表后,就可以通过循环来重新初始化所有的 manager 了。
5、实现恢复状态的代理方法
在重新初始化 manager 之后,接下来需要同步 Core Bluetooth 存储的他们的状态。要想弄清楚在程序被退出时都在做些什么,就需要正确的实现代理方法。对于 central manager 来说,需要实现centralManager:willRestoreState:;对于 peripheral manager 来说,需要实现peripheralManager:willRestoreState:。
注意:如果选择存储和恢复状态,当系统在后台重新加载程序时,首先调用的方法是centralManager:willRestoreState:或peripheralManager:willRestoreState:。如果没有选择存储的恢复状态(或者唤醒时没有什么内容需要恢复),那么首先调用的方法是centralManagerDidUpdateState:或peripheralManagerDidUpdateState:。
无论是以上哪种代理方法,最后一个参数都是一个包含程序退出前状态的字典。字典中,可用的 key ,central 端有:
peripheral 端有:
要恢复 central manager 的状态,可以用centralManager:willRestoreState:返回字典中的 key 来得到。假如说 central manager 有想要或者已经连接的 peripheral,那么可以通过 CBCentralManagerRestoredStatePeripheralsKey 对应得到的 peripheral(CBPeripheral 对象)数组来得到。
具体要对拿到的 peripheral 数组做什么就要根据需求来了。如果这是个 central manager 搜索到的 peripheral 数组,那就可以存储这个数组的引用,并且开始建立连接了(注意给这些 peripheral 设置代理,否则连接后不会走 peripheral 的代理方法)。
恢复 peripheral manager 的状态和 central manager 的方式类似,就只是把代理方法换成了peripheralManager:willRestoreState:,并且使用对应的 key 即可。
6、更新 manager 初始化过程
在实现了全部的必须步骤后,你可能想要更新 manager 的初始化过程。虽然这是个可选的操作,但是它对确保各种操作能正常进行尤为重要。假如,你的应用在 central 和 peripheral 做数据交互时,被强制退出了。即使 app 最后恢复状态时,找到了这个 peripheral,那你也不知道 central 和这个 peripheral 当时的具体状态。但其实我们在恢复时,是想恢复到程序被强制退出前的那一步。
这个需求,可以在代理方法centralManagerDidUpdateState:中,通过发现恢复的 peripheral 是否之前已经成功连接来实现:
上面的代码描述了,当系统在完成搜索 service 之后才退出的程序,可以通过调用discoverServices:方法来恢复 peripheral 的数据。如果 app 成功搜索到 service,你可以是否能搜索到需要的 characteristic(或者已经订阅过)。通过更新初始化过程,可以确保在正确的时间点,调用正确的方法。
7、第三方框架
iOS 蓝牙开发中常用的第三方框架
作者:xiaofu666
链接:https://www.jianshu.com/p/42b7c318ee24
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
转载声明,仅为个人学习使用,翻阅原作者的上一篇,发现不能阅读了,所以就把本篇转载一下。