iOS 蓝牙4.0开发踩坑总结

蓝牙基础

IOS中关于蓝牙的框架其实有四个:

(1)GameKit.framework 根据名称我们可以猜出,这是个游戏开发API,仅限于ios设备之间的连接。

(2)MultipeerConnectivity.framework iOS7将GameKit中的蓝牙模块单独出的一个Multipeer Connectivity Framework,通过发现附近的设备用wifi或蓝牙进行p2p连接,限ios设备之间互相传文件用的。

(3)ExternalAccessory.framework 用于和第三方蓝牙进行交互,必须是MFI认证的设备。

(4)CoreBluetooth.framework 这就是我们的要细细研究的了,主要用于和第三方蓝牙的交互,必须是蓝牙4.0以上的设备,蓝牙4.0也叫BLE(Bluetooth Low Energy)所以一般都称之为BlE开发,从iPhone4s及其以后的设备都是支持BLE的。

蓝牙开发分为中心者模式和管理者模式. 我们绝大多数App使用的都是中心者模式,这次先来讲一下在中心者模式开发基本流程.

1.创建中心设备管理器

// 创建中心设备管理器,会回调centralManagerDidUpdateState
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             //蓝牙power没打开时alert提示框 iOS11设置页里关闭才会弹
                             [NSNumber numberWithBool:YES],CBCentralManagerOptionShowPowerAlertKey, @"amigoCentralManagerIdentifier",CBCentralManagerOptionRestoreIdentifierKey,nil];
    
    NSArray *backgroundModes = [[[NSBundle mainBundle] infoDictionary]objectForKey:@"UIBackgroundModes"];
    if ([backgroundModes containsObject:@"bluetooth-central"]) {
        //info.plist 有声明蓝牙使用 后台模式
        self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:options];
    }
    else {
        //非后台模式
        self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    }

2.获取到蓝牙状态

//只要中心管理者初始化 就会触发此代理方法 判断手机蓝牙状态
- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    NSLog(@"检测到当前蓝牙状态::%ld",central.state);
    if (self.stateBlock) {
        self.stateBlock(central.state);
    }
}

3.扫描外设

// 根据SERVICE_UUID来扫描外设,如果不设置SERVICE_UUID,则扫描所有蓝牙设备 正常业务我们只识别自己的服务厂商的UUID
[self.centralManager.defaultCentralManager scanForPeripheralsWithServices:self.serviceUUIDs options:nil];

4.发现服务,扫描指定外设的特征值

/** 发现符合要求的外设,回调 */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI {
    //连接指定的设备 通过匹配设备名称或其他方式
    NSString *macKey = [advertisementData objectForKey:@"kCBAdvDataLocalName"];
    if (!ISEmptyString(macKey) && [self.deviceName isEqualToString:macKey]) {
        NSLog(@"扫描到目标外设 准备连接:::%ld::UUIDString:%@",peripheral.state,[peripheral identifier].UUIDString);
        if (peripheral.state == CBPeripheralStateConnecting) {
            self.connectState = XLBluetoothConnectStateConnectFailed;
            if ([_delegate respondsToSelector:@selector(centralManagerBluetoothConnectState:)]) {
                [_delegate centralManagerBluetoothConnectState:XLBluetoothConnectStateConnectFailed];
            }
        }
        else
        {
            self.peripheral = peripheral;
            self.connectState = XLBluetoothConnectStateConnecting;
            if ([_delegate respondsToSelector:@selector(centralManagerBluetoothConnectState:)]) {
                [_delegate centralManagerBluetoothConnectState:XLBluetoothConnectStateConnecting];
            }
            //连接外设
            [self.centralManager.defaultCentralManager connectPeripheral:peripheral options:nil];
        }
    }
}

5.连接外设成功,寻找服务

/** 连接成功 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    // 可以停止扫描
    NSLog(@"连接成功 停止扫描");
    [self.centralManager.defaultCentralManager stopScan];
    
    self.connectState = XLBluetoothConnectStateConnectSuccessed;
    if ([_delegate respondsToSelector:@selector(centralManagerBluetoothConnectState:)]) {
        [_delegate centralManagerBluetoothConnectState:XLBluetoothConnectStateConnectSuccessed];
    }
    // 设置代理
    _peripheral.delegate = self;
    // 根据UUID来寻找服务
    [_peripheral discoverServices:self.serviceUUIDs];
}

6.发现服务

/** 发现服务 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    // 遍历出外设中所有的服务
    for (CBService *service in peripheral.services) {
        NSLog(@"所有的服务:%@",service);
        // 根据UUID寻找服务中的特征 连接某个设备 serviceUUID 就只能是单个的
        if ([service.UUID isEqual:self.serviceUUIDs.firstObject]) {
            // characteristicUUIDs : 可以指定想要扫描的特征(传nil,扫描所有的特征)
            [peripheral discoverCharacteristics:nil forService:service];
        }
    }
}

7.发现特征回调

/** 发现特征回调 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    NSLog(@"service.characteristics::%ld",service.characteristics.count);
    // 遍历出所需要的特征
    for (CBCharacteristic *characteristic in service.characteristics) {
        if (characteristic.properties & CBCharacteristicPropertyRead) {
            // 直接读取这个特征数据,会调用didUpdateValueForCharacteristic
            [peripheral readValueForCharacteristic:characteristic];
        }
        if ((characteristic.properties & CBCharacteristicPropertyNotify) || (characteristic.properties & CBCharacteristicPropertyIndicate)) {
            // 订阅通知
            self.notifCharacteristic = characteristic;
            [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        }
        if (characteristic.properties & CBCharacteristicPropertyWriteWithoutResponse) {
            NSLog(@"Properties is Write");
            self.writeCharacteristic = characteristic;
            //必须等notifCharacteristic 注册了之后才能去写数据 否则数据结果会没有回调
            if (!ISEmptyString(self.command) && _notifCharacteristic) {
                [self writeCommandToDevice:self.command];
            }
            //            [peripheral discoverDescriptorsForCharacteristic:characteristic];
        }
        
        NSLog(@"the property :%lu",(unsigned long)characteristic.properties );
    }
}

8.订阅状态改变,可以开始写数据

/** 订阅状态的改变 */
-(void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {

    if (error == nil) {
        if (characteristic.isNotifying) {
            NSLog(@"订阅成功");
            if ([self.delegate respondsToSelector:@selector(didUpdateNotificationStateSuccess)]) {
                [self.delegate didUpdateNotificationStateSuccess];
            }
            
            /** 如果有命令未下发的,订阅成功可以开始写数据了**/
            if (!ISEmptyString(self.command)) {
                [self writeCommandToDevice:self.command];
            }
            
        }
    }
}

9.接收蓝牙数据

/** 接收到数据回调 */
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    // 拿到外设发送过来的数据 只接受指定notif的特征值
    if (![characteristic isEqual:_notifCharacteristic]) {
        return;
    }
    
    if (error == nil) {
         //蓝牙回复的内容 16进制的Data 需要转成String
        NSData *data = characteristic.value;
        NSString *content = [NSString convertDataToHexStr:data];
        if ([_delegate respondsToSelector:@selector(peripheralReportContent:)]) {
            [_delegate peripheralReportContent:content];
        }
        NSLog(@"didUpdateValueForCharacteristic ::%@",content);
    }
}

10 蓝牙断开连接

//主动断开蓝牙连接
[self.centralManager cancelPeripheralConnection:_peripheral];

/** 断开连接 回调 */
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error {
    NSLog(@"断开连接");
}

蓝牙中心者模式开发的基本流程就是这样的了,这个过程蓝牙数据传输的格式是十六进制的NSData,发送一般20字节一次(这个是由BLE的MTU决定的),如果想要传输更多字节数,可以采用分包等方式,一般的蓝牙功能20字节也是够用了,具体的传输协议需要和蓝牙的硬件开发商协调沟通,一般都会有个说明书.

现在说一下,在整个蓝牙开发过程中遇到过的两个问题:
1.CBCharacteristicWriteWithResponseCBCharacteristicWriteWithoutResponse的选择.
在往蓝牙的某个特征值写入数据时用到
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;

从字面意思上解析:
CBCharacteristicWriteWithResponse: 特征值写入数据会有相应.
CBCharacteristicWriteWithoutResponse: 特征值写入数据不会有响应.

其实这个选择是有特征值权限所决定的:

typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) {
    CBCharacteristicPropertyBroadcast                                               = 0x01,
    CBCharacteristicPropertyRead                                                    = 0x02,
    CBCharacteristicPropertyWriteWithoutResponse                                    = 0x04,
    CBCharacteristicPropertyWrite                                                   = 0x08,
    CBCharacteristicPropertyNotify                                                  = 0x10,
    CBCharacteristicPropertyIndicate                                                = 0x20,
    CBCharacteristicPropertyAuthenticatedSignedWrites                               = 0x40,
    CBCharacteristicPropertyExtendedProperties                                      = 0x80,
    CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0)   = 0x100,
    CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(10_9, 6_0) = 0x200
};

特征值权限可以是多个结合,如读写共存.


蓝牙特征值.jpg

但是我在实际开发过程中使用CBCharacteristicWriteWithResponse或CBCharacteristicWriteWithoutResponse对流程没有一点差异,后来知道,
在往蓝牙设备中写入数据时,

2.这是一个比较讨厌还未知道真正原因的问题,当时在做蓝牙功能开发的时候,蓝牙模块代码封装中,CBCentralManager对象是维持一个,还是每一次用到蓝牙功能都去初始化一下,在系统控制台中看到,每一次生成一个CBCentralManager对象,都会输出以下日志:

    Jan 23 21:36:36 hende-iPhone blueTest(CoreBluetooth)[4533] <Error>: API MISUSE: <private> has no restore identifier but the delegate implements the centralManager:willRestoreState: method. Restoring will not be supported
    Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Received XPC message "CBMsgIdCheckIn" from session ""
    Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Received XPC check-in from session "com.wesk.blueTest-central-4533-0"
    Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Sending 'session attached' event for session "com.wesk.blueTest-central-4533-0"
    Jan 23 21:36:36 hende-iPhone BTServer[61] <Notice>: Registering central session "com.wesk.blueTest-central-4533-0" with backgrounding: on, persistence: off

可以看到一个CBCentralManager对象,对手机而言,就是一个session会话,如果每一次使用完之后即时释放该对象,应该不会有什么问题,而且CBCentralManager对象每一次初始化的话,手机系统会对蓝牙权限没有打开的用户弹出提示,这样做设计上更人性化.

开发测试上线,公司测试通过,没有出现任何问题,然后上线,一个月后iOS手机用户基数达到2000+了,有一个iPhoneX iOS11.2的用户反馈说app蓝牙用不了,打开手机点击使用蓝牙功能,提示蓝牙权限未打开,通过网上所说的各种蓝牙解决方案,多次开关蓝牙按钮,手机飞行模式,手机重启... 还是提示权限未打开!当问题到这儿了,我心中是一万个不相信是代码层面问题,因为2000多的用户就这么一个出问题.
随着用户的增长,有一个iPhone7反馈蓝牙也不能正常,使用提示打开蓝牙权限,问题就变得严重起来了,需要彻底解决这个问题.
为了继续追踪这个问题,,特地加了土豪用户为好友,让其帮忙测试找问题,经过来回几轮验证,该手机拿到系统蓝牙权限的回调都是CBManagerStatePoweredOff,期间为了保证环境干净,专门写了一个获取蓝牙权限的demo,一打开App就获取蓝牙权限,状态实时提示,让他装起来测试,权限获取居然是正常的,这不是说明我的代码有问题!!! 心中一万个急啊,不该啊,蓝牙代码使用都一样,各种比较,从工程配置到代码细节,最后得出一个可能的结论“CBCentralManager对象在一个app中不能多次生成,仅保持一个CBCentralManager对象”. 花了一些时间,将CBCentralManager对象单例化,重新打包给iPhoneX用户使用,终于正常了!

但是其原因到现在还是不能非常很好的理解,因为出现这个问题的手机实在太少,且都是iOS11以上版本,就那么两只,当时3200+的iPhone手机用户,出现该问题的手机低于千分之一,让我不得不怀疑是手机硬件蓝牙问题不兼容.苹果也没有指出CBCentralManager对象不可以同时存在多个.

上面第一个问题是开发过程中遇到过的坑,第二个是一个比较严重的问题,应该可以归结于代码使用姿势了(虽然不知道原因,但为了避免出现手机蓝牙权限获取不正确的场景,CBCentralManager对象还是使用单例模式吧).

PS:iOS11蓝牙开关分未设置页和控制中心的,设置页是总开关(系统级使用),控制中心是给各个APP使用的, 控制中心的开关有时候会显示异常,显示打开,但权限其实是关闭的.如下图

iOS11展示蓝牙异常.gif

查找解决期间在官方bug反馈上也看到了该问题:https://forums.developer.apple.com/thread/92997.

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

推荐阅读更多精彩内容

  • iOS开发蓝牙4.0初识转载 2015-09-20 15:26:44标签:ios开发蓝牙ios开发蓝牙4.0ios...
    Jany_4a9a阅读 2,716评论 0 3
  • 什么是蓝牙? 随着蓝牙低功耗技术BLE(Bluetooth Low Energy)的发展,蓝牙技术正在一步步成熟,...
    一字码阅读 1,740评论 0 11
  • 安静的夜,同样的失眠,一个人开着车漫无目的的走着,下一站是哪?会遇到谁?谁知道呢?突然想起《从你的全世界路过》里面...
    black_swan阅读 621评论 1 1
  • 有国人人说:我砸烂了我的苹果手机,改用国产手机,看看我是多么爱国!;有国人说,我砸烂了别人的进口汽车,还打伤了...
    碧海飞鸿2016阅读 302评论 0 2