蓝牙基础
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.CBCharacteristicWriteWithResponse和CBCharacteristicWriteWithoutResponse的选择.
在往蓝牙的某个特征值写入数据时用到
- (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
};
特征值权限可以是多个结合,如读写共存.
但是我在实际开发过程中使用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使用的, 控制中心的开关有时候会显示异常,显示打开,但权限其实是关闭的.如下图
查找解决期间在官方bug反馈上也看到了该问题:https://forums.developer.apple.com/thread/92997.