CoreBluetooth 蓝牙开发(后台模式、状态保存与恢复)

最近新进一家公司,主要是做物联网这一块的的,项目需要用到蓝牙开发,讲真的,挑战还是挺大的,做了差不多四年的iOS开发,从没有接触过蓝牙开发这一领域,我是这样学习的。

从网上找各种博客(国内的,国外的),借鉴别人写过的Demo以及官方文档,花了整整的一周时间,对iOS的CoreBluetooth这个框架的使用稍微有一些的了解,请听我一一道来;

iOS 蓝牙

简称:BLE(buletouch low energy),蓝牙 4.0 设备因为低耗电,所以也叫做 BLE,CoreBluetooth框架就是苹果公司为我们提供的一个库,我们可以使用这个库和其他支持蓝牙4.0的设备进行数据交互。值得注意的是在IOS10之后的APP中,我们需要在 info.plist文件中添加NSBluetoothPeripheralUsageDescription字段否则APP会崩溃

工作模式:蓝牙通信中,首先需要提到的就是 central 和 peripheral 两个概念。这是设备在通信过程中扮演的两种角色。直译过来就是 [中心] 和 [周边(可以理解为外设)]。iOS 设备既可以作为 central,也可以作为 peripheral,这主要取决于通信需求。

自己尝试的写了个Demo,实现的功能有:

1、通过已知外围设备的服务UUID搜索(这个UUID是指被广播出来的服务UUID);
2、连接指定的外围设备;
3、获取指定的服务,发现需要订阅的特征;
4、接收外围设备发送的数据;
5、向外围设备写数据;
6、实现蓝牙服务的后台模式;
7、实现蓝牙服务的状态保存与恢复(应用被系统杀死的时候,系统会自动保存 central manager 的状态);

中心角色的实现:(central)

(1)、初始化中央管理器对象

/**
第一个参数:代理
第二个参数:队列(nil为不指定队列,默认为主队列)
第三个参数:实现状态保存的时候需要用到 eg:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"} 
*/  
centerManager = [[CBCentralManager alloc]initWithDelegate:self queue:queue options:options];

中央管理器会调用 centralManagerDidUpdateState:通知蓝牙的状态

(2)、发现外围设备

[centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:SERVICE_UUID]] options:nil];

每次中央管理器发现外围设备时,它都会调用centralManager:didDiscoverPeripheral:advertisementData:RSSI:其委托对象的方法。

(3)、发现想要的外围设备进行连接

#pragma mark -- 扫描发现到任何一台设备都会通过这个代理方法回调
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    //过滤掉无效的结果
    if (peripheral == nil||peripheral.identifier == nil/*||peripheral.name == nil*/)
    {
        return;
    }
    
    NSString *pername =[NSString stringWithFormat:@"%@",peripheral.name];
    NSLog(@"所有服务****:%@",peripheral.services);

    NSLog(@"蓝牙名字:%@  信号强弱:%@",pername,RSSI);
   //连接需要的外围设备
    [self connectPeripheral:peripheral];
    //将搜索到的设备添加到列表中
    [self.peripherals addObject:peripheral];
    
    if (_didDiscoverPeripheralBlock) {
        _didDiscoverPeripheralBlock(central,peripheral,advertisementData,RSSI);
    }
}

如果连接请求成功,则中央管理器调用centralManager:didConnectPeripheral:其委托对象的方法。

(4)、发现所连接的外围设备的服务

#pragma mark -- 连接成功、获取当前设备的服务和特征 并停止扫描
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSLog(@"%@",peripheral);
    
    // 设置设备代理
    [peripheral setDelegate:self];
    // 大概获取服务和特征
    [peripheral discoverServices:@[[CBUUID UUIDWithString:SERVICE_UUID]]];
    
    NSLog(@"Peripheral Connected");
    
    if (_centerManager.isScanning) {
        [_centerManager stopScan];
    }
    NSLog(@"Scanning stopped");
    
}

发现指定的服务时,外围设备(CBPeripheral你连接的对象)会调用peripheral:didDiscoverServices:其委托对象的方法。

(5)、发现服务的特征

#pragma mark -- 获取当前设备服务services
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    if (error) {
        NSLog(@"Error discovering services: %@", [error localizedDescription]);
        return;
    }
    NSLog(@"所有的servicesUUID%@",peripheral.services);   
    //遍历所有service
    for (CBService *service in peripheral.services)
    {
        NSLog(@"服务%@",service.UUID);
        //找到你需要的servicesuuid
        if ([[NSString stringWithFormat:@"%@",service.UUID] isEqualToString:SERVICE_UUID])
        {
            // 根据UUID寻找服务中的特征
            [peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:CHARACTERISTIC_UUID]] forService:service];
        }
    }
}

peripheral:didDiscoverCharacteristicsForService:error:当发现指定服务的特征时,外围设备调用其委托对象的方法。

(6)、检索特征价值

阅读特征的值 ()

 [peripheral readValueForCharacteristic:interestingCharacteristic];

注意: 并非所有特征都是可读的。你可以通过检查其properties属性是否包含CBCharacteristicPropertyRead常量来确定特征是否可读。如果尝试读取不可读的特征值,则peripheral:didUpdateValueForCharacteristic:error:委托方法将返回合适的错误。

订阅特征的值()

虽然使用该readValueForCharacteristic:方法读取特征值对静态值有效,但它不是检索动态值的最有效方法。检索随时间变化的特征值 - 例如,你的心率 - 通过订阅它们。订阅特征值时,您会在值更改时收到外围设备的通知。

[peripheral setNotifyValue:YES forCharacteristic:interestingCharacteristic];

注意: 并非所有特征都提供订阅。你可以通过检查特性是否properties包含其中一个CBCharacteristicPropertyNotify或多个CBCharacteristicPropertyIndicate常量来确定特征是否提供订阅。
当你订阅(或取消订阅)特征的值时,外围设备会调用peripheral:didUpdateNotificationStateForCharacteristic:error:其委托对象的方法。

写一个特征的值 ()

有时写一个特征的值是有意义的。例如,如果你的应用程序与蓝牙低功耗数字恒温器交互,你可能需要为恒温器提供设置房间温度的值。如果特征值是可写的,则可以NSData通过调用外设writeValue:forCharacteristic:type:方法将数据值;

[self.discoveredPeripheral writeValue:data forCharacteristic:self.characteristic1 type:CBCharacteristicWriteWithResponse];

写入特征的值时,指定要执行的写入类型。在上面的示例中,写入类型CBCharacteristicWriteWithResponse指示外围设备通过调用peripheral:didWriteValueForCharacteristic:error:其委托对象的方法让您的应用程序知道写入是否成功。

外围角色的实现

(1)、初始化外围设备管理器

peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil];

创建外围设备管理器时,外围设备管理器会调用peripheralManagerDidUpdateState:其委托对象的方法。您必须实现此委托方法,以确保支持蓝牙低功耗并可在本地外围设备上使用。

(2)、设置服务和特征

为自定义服务和特征创建自己的UUID
在终端使用 uuidgen 命令获取以ASCII字符串形式的128位值的UUID:71DA3FD1-7E10-41C1-B16F-4430B506CDE7

构建服务树和特征

myCharacteristic =[[CBMutableCharacteristic alloc] initWithType:myCharacteristicUUID properties:CBCharacteristicPropertyRead value:myValue permissions:CBAttributePermissionsReadable];   //特征
 myService = [[CBMutableService alloc] initWithType:myServiceUUID primary:YES];    //与特征所关联的服务

myService.characteristics = @ [myCharacteristic];        //设置服务的特征数组,将特征与其关联

(3)、发布服务和特征

  [peripheralManager addService:myService];

当调用此方法发布服务时,外围管理器将调用peripheralManager:didAddService:error:其委托对象的方法。通过error可以知道是否发布成功;
将服务及其任何关联特性发布到外围设备的数据库后,该服务将被缓存,将无法再对其进行更改。

(4)、广播服务

  [peripheralManager startAdvertising:@ {CBAdvertisementDataServiceUUIDsKey:@[myFirstService.UUID,mySecondService.UUID]}];

当开始在本地外围设备上公布某些数据时,外围设备管理器会调用peripheralManagerDidStartAdvertising:error:其委托对象的方法。

(5)、响应来自中央的读取和写入请求

当连接的中央请求读取某个特征的值时,外围管理器会调用peripheralManager:didReceiveReadRequest:其委托对象的方法。

 [peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset]; 

设置读取请求不要求从超出特征值的边界的索引位置读取

  request.value = [myCharacteristic.value subdataWithRange:NSMakeRange(request.offset,myCharacteristic.value.length  -  request.offset)];  

将请求的特性属性(默认值为nil)的值设置为您在本地外围设备上创建的特征值,同时考虑读取请求的偏移量

设置值后,响应远程中央以指示请求已成功完成。通过调用类的respondToRequest:withResult:方法CBPeripheralManager,传回请求(其更新的值)和请求的结果

当连接的中心发送写入一个或多个特征值的请求时,外围管理器会调用peripheralManager:didReceiveWriteRequests:其委托对象的方法

(6)、将更新的特征值发送到订阅的中心

当连接的中心订阅某个特征的值时,外围管理器会调用peripheralManager:central:didSubscribeToCharacteristic:其委托对象的方法
获取特征的更新值,并通过调用类的updateValue:forCharacteristic:onSubscribedCentrals:方法将其发送到中心CBPeripheralManager。

处理常驻后台任务

首先需要在Capabilities-->Background Modes申请中心角色的后台模式说明

如图:


中心角色后台模式.jpg

(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)、选择支持存储和恢复

如果要支持存储和恢复,则需要在初始化 manager 的时候给一个 restoration identifier。restoration identifier 是 string 类型,并标识了 app 中的 central manager 或 peripheral manager。这个 string 很重要,它将会告诉 Core Bluetooth 需要存储状态,毕竟 Core Bluetooth 恢复有 identifier 的对象。

例如,在 central 端,要想支持该特性,可以在调用 CBCentralManager 的初始化方法时,配置 CBCentralManagerOptionRestoreIdentifierKey:

centralManager = [[CBCentralManager alloc] initWithDelegate:self 
queue:nil
options:@{CBCentralManagerOptionRestoreIdentifierKey:@"centralManagerIdentifier"}];

虽然以上代码没有展示出来,其实在 peripheral manager 中要设置 identifier 也是这样的。只是在初始化时,将 key 改成了 CBPeripheralManagerOptionRestoreIdentifierKey。
因为程序可以有多个 CBCentralManager 和 CBPeripheralManager,所以要确保每个 identifier 都是唯一的。

(3)、重新初始化 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];
    if (centralManagerIdentifiers.count) {
        //重新初始化所有的 manager 
        for (NSString *identifier in centralManagerIdentifiers) {
            NSLog(@"系统启动项目");
            //在这里创建的蓝牙实例一定要被当前类持有,不然出了这个函数就被销毁了,蓝牙检测会出现“XPC connection invalid”
            self.bluetooth = [[MSBBlueTooth alloc]initWithQueue:nil options:@{CBCentralManagerOptionRestoreIdentifierKey : identifier}];
            NSLog(@"");
        }
    }

return YES;
}

(4)、实现恢复状态的代理方法

在重新初始化 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 = dict[CBCentralManagerRestoredStatePeripheralsKey];
    //讲状态保存的设备加入列表,在蓝牙检测状态的回调里实现重连
    self.peripherals = [NSMutableArray arrayWithArray:peripherals];

}

具体要对拿到的 peripheral 数组做什么就要根据需求来了。如果这是个 central manager 搜索到的 peripheral 数组,那就可以存储这个数组的引用,并且开始建立连接了(注意给这些 peripheral 设置代理,否则连接后不会走 peripheral 的代理方法)。
.
恢复 peripheral manager 的状态和 central manager 的方式类似,就只是把代理方法换成了 peripheralManager:willRestoreState:,并且使用对应的 key 即可

写的不是很好,也算是东拼西凑了,但也是花了时间去整理的,如果看不懂,可以下载我的Demo自己跑一遍;

想要看实现效果,可以下载Demo,看的再多也不如项目跑一遍来的快,疗效是不骗人的;

有需要的可以加我微信Jarvis-LLL,一起讨论学习

喜欢就点个赞,也可以在下方评论一起讨论讨论

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容