ios基础技术类

iOS 蓝牙4.0开发踩坑总结

2018-01-24  本文已影响1059人  wesk痕

蓝牙基础

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.

上一篇下一篇

猜你喜欢

热点阅读