MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的Core Bluetooth蓝牙通信

2022-10-292.9k 阅读

Core Bluetooth框架概述

Core Bluetooth 框架是苹果公司为 iOS 和 macOS 开发者提供的用于与蓝牙低功耗(BLE)设备进行通信的框架。在 Objective-C 开发中,Core Bluetooth 使得与 BLE 设备交互变得相对容易,无论是从设备发现、连接,到服务与特征的读写操作,都有一套相对完善的 API 支持。

核心类介绍

  1. CBCentralManager:中心角色管理器,负责管理中心设备的状态,如扫描周边的 BLE 设备。例如,初始化一个中心管理器:
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];

这里通过 initWithDelegate:queue: 方法初始化了一个 CBCentralManager 实例,并指定了代理 self,队列设置为 nil 表示使用主线程队列。

  1. CBPeripheral:代表一个周边设备,一旦中心设备发现了周边设备,就会得到 CBPeripheral 实例。例如,在中心管理器代理方法中获取周边设备:
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
    // 这里的peripheral就是发现的周边设备实例
}
  1. CBService:每个 CBPeripheral 可以包含多个服务,这些服务定义了设备的功能。比如心率监测设备可能有心率服务。获取周边设备的服务:
[peripheral discoverServices:nil];

这里调用 discoverServices: 方法,参数 nil 表示发现所有服务。

  1. CBCharacteristic:每个服务可以包含多个特征,特征是数据的载体。例如心率服务中的心率值特征。发现服务中的特征:
[peripheral discoverCharacteristics:nil forService:service];

同样,参数 nil 表示发现服务中的所有特征。

中心模式开发流程

初始化中心管理器

首先,在项目中需要初始化 CBCentralManager。这通常在视图控制器的 viewDidLoad 方法或者其他合适的初始化位置进行。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}

在初始化时,指定代理 self,这意味着当前视图控制器需要遵循 CBCentralManagerDelegate 协议。该协议包含了一系列方法,用于处理中心管理器的状态变化以及设备发现等事件。

处理中心管理器状态变化

中心管理器有多种状态,如未授权、未初始化、准备好等。我们需要在代理方法中处理这些状态变化。

- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    switch (central.state) {
        case CBCentralManagerStateUnknown:
            NSLog(@"Central manager state is unknown");
            break;
        case CBCentralManagerStateResetting:
            NSLog(@"Central manager state is resetting");
            break;
        case CBCentralManagerStateUnsupported:
            NSLog(@"BLE is unsupported on this device");
            break;
        case CBCentralManagerStateUnauthorized:
            NSLog(@"App is not authorized to use BLE");
            break;
        case CBCentralManagerStatePoweredOff:
            NSLog(@"BLE is powered off");
            break;
        case CBCentralManagerStatePoweredOn: {
            NSLog(@"BLE is powered on, start scanning");
            [self.centralManager scanForPeripheralsWithServices:nil options:nil];
        }
            break;
        default:
            break;
    }
}

CBCentralManagerStatePoweredOn 状态下,我们调用 scanForPeripheralsWithServices:options: 方法开始扫描周边设备。参数 nil 表示扫描所有服务,options 参数可以用于设置扫描的一些选项,如是否允许重复扫描等。

扫描并发现周边设备

当中心管理器开始扫描后,一旦发现周边设备,就会调用代理方法 centralManager:didDiscoverPeripheral:advertisementData:RSSI:

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
    NSLog(@"Discovered peripheral: %@, RSSI: %@", peripheral.name, RSSI);
    // 可以在这里对发现的设备进行筛选,比如根据设备名称或UUID
    if ([peripheral.name containsString:@"TargetDevice"]) {
        [self.centralManager stopScan];
        self.discoveredPeripheral = peripheral;
        self.discoveredPeripheral.delegate = self;
        [self.centralManager connectPeripheral:self.discoveredPeripheral options:nil];
    }
}

在这个方法中,我们首先打印出发现的设备名称和信号强度(RSSI)。然后,通过判断设备名称是否包含特定字符串来筛选目标设备。如果找到了目标设备,就停止扫描,保存发现的周边设备实例,并设置其代理为当前视图控制器(当前视图控制器需要遵循 CBPeripheralDelegate 协议),最后连接到该设备。

连接周边设备

连接周边设备通过 connectPeripheral:options: 方法进行。连接成功或失败都会触发相应的代理方法。

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"Connected to peripheral: %@", peripheral.name);
    [peripheral discoverServices:nil];
}

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    NSLog(@"Failed to connect to peripheral: %@, error: %@", peripheral.name, error);
}

didConnectPeripheral: 方法中,连接成功后我们开始发现设备的服务。而在 didFailToConnectPeripheral:error: 方法中,我们打印出连接失败的设备名称和错误信息。

发现服务与特征

一旦连接成功并开始发现服务,当服务发现完成后,会调用 peripheral:didDiscoverServices: 代理方法。

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
    if (error) {
        NSLog(@"Error discovering services: %@", error);
        return;
    }
    for (CBService *service in peripheral.services) {
        NSLog(@"Discovered service: %@", service.UUID);
        [peripheral discoverCharacteristics:nil forService:service];
    }
}

在这个方法中,我们首先检查是否有错误发生。如果没有错误,就遍历设备的所有服务,并打印出服务的 UUID。然后,针对每个服务,调用 discoverCharacteristics:forService: 方法来发现服务中的特征。当特征发现完成后,会调用 peripheral:didDiscoverCharacteristicsForService:error: 代理方法。

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
    if (error) {
        NSLog(@"Error discovering characteristics: %@", error);
        return;
    }
    for (CBCharacteristic *characteristic in service.characteristics) {
        NSLog(@"Discovered characteristic: %@", characteristic.UUID);
        // 这里可以根据特征UUID进一步判断是否是我们需要的特征
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"YOUR_CHARACTERISTIC_UUID"]]) {
            // 对特定特征进行操作,比如读取或设置通知
        }
    }
}

同样,先检查错误,然后遍历服务中的所有特征并打印其 UUID。如果找到了特定 UUID 的特征,就可以对其进行相应的操作,如读取数据或设置通知。

读取与写入特征值

  1. 读取特征值:要读取特征值,首先要确保特征支持读取操作(可以通过 characteristic.properties 判断)。然后调用 readValueForCharacteristic: 方法。
if (characteristic.properties & CBCharacteristicPropertyRead) {
    [peripheral readValueForCharacteristic:characteristic];
}

当读取操作完成后,会调用 peripheral:didUpdateValueForCharacteristic:error: 代理方法。

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error reading characteristic value: %@", error);
        return;
    }
    NSData *data = characteristic.value;
    // 根据数据格式进行解析,比如如果是NSString格式
    NSString *stringValue = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Read characteristic value: %@", stringValue);
}
  1. 写入特征值:写入特征值同样要先判断特征是否支持写入操作(通过 characteristic.properties 判断)。然后调用 writeValue:forCharacteristic:type: 方法。
if (characteristic.properties & CBCharacteristicPropertyWrite) {
    NSData *dataToWrite = [@"Hello, BLE device" dataUsingEncoding:NSUTF8StringEncoding];
    [peripheral writeValue:dataToWrite forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
}

这里的 type 参数有两种选择,CBCharacteristicWriteWithResponse 表示写入后等待设备响应,CBCharacteristicWriteWithoutResponse 表示不等待响应直接写入。写入操作完成后,如果是 CBCharacteristicWriteWithResponse 类型,会调用 peripheral:didWriteValueForCharacteristic:error: 代理方法。

- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error writing characteristic value: %@", error);
    } else {
        NSLog(@"Characteristic value written successfully");
    }
}

设置特征通知

为了实时获取设备数据,通常需要设置特征通知。首先要确保特征支持通知操作(通过 characteristic.properties 判断)。然后调用 setNotifyValue:forCharacteristic: 方法。

if (characteristic.properties & CBCharacteristicPropertyNotify) {
    [peripheral setNotifyValue:YES forCharacteristic:characteristic];
}

当设备有新的数据更新时,会调用 peripheral:didUpdateValueForCharacteristic:error: 代理方法,和读取特征值后的回调方法相同,在这里可以处理新的数据。

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error receiving characteristic update: %@", error);
        return;
    }
    NSData *data = characteristic.value;
    // 解析数据并处理
    // 例如,如果是温度数据,假设数据格式为float
    float temperature;
    [data getBytes:&temperature length:sizeof(float)];
    NSLog(@"Received temperature: %f", temperature);
}

周边模式开发流程

初始化周边管理器

在周边模式下,需要初始化 CBPeripheralManager。同样,在合适的初始化位置进行。

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

这里也指定了代理 self,当前视图控制器需要遵循 CBPeripheralManagerDelegate 协议。

处理周边管理器状态变化

周边管理器也有多种状态,通过代理方法 peripheralManagerDidUpdateState: 处理。

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral {
    switch (peripheral.state) {
        case CBPeripheralManagerStateUnknown:
            NSLog(@"Peripheral manager state is unknown");
            break;
        case CBPeripheralManagerStateResetting:
            NSLog(@"Peripheral manager state is resetting");
            break;
        case CBPeripheralManagerStateUnsupported:
            NSLog(@"BLE is unsupported on this device for peripheral mode");
            break;
        case CBPeripheralManagerStateUnauthorized:
            NSLog(@"App is not authorized to use BLE in peripheral mode");
            break;
        case CBPeripheralManagerStatePoweredOff:
            NSLog(@"BLE is powered off for peripheral mode");
            break;
        case CBPeripheralManagerStatePoweredOn: {
            NSLog(@"BLE is powered on for peripheral mode, start advertising");
            [self startAdvertising];
        }
            break;
        default:
            break;
    }
}

CBPeripheralManagerStatePoweredOn 状态下,调用 startAdvertising 方法开始广播。

创建服务与特征

在广播之前,需要创建服务和特征。首先创建一个服务:

CBService *myService = [[CBService alloc] initWithType:[CBUUID UUIDWithString:@"YOUR_SERVICE_UUID"] primary:YES];

然后创建特征:

CBCharacteristic *myCharacteristic = [[CBCharacteristic alloc] initWithType:[CBUUID UUIDWithString:@"YOUR_CHARACTERISTIC_UUID"] properties:CBCharacteristicPropertyRead | CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];

这里设置了特征的属性为可读和可通知,权限为可读。将特征添加到服务中:

myService.characteristics = @[myCharacteristic];

最后将服务添加到周边管理器中:

[self.peripheralManager addService:myService];

开始广播

广播是让周边设备能够被中心设备发现的关键步骤。

- (void)startAdvertising {
    NSDictionary *advertisementData = @{
        CBAdvertisementDataServiceUUIDsKey: @[[CBUUID UUIDWithString:@"YOUR_SERVICE_UUID"]],
        CBAdvertisementDataLocalNameKey: @"My BLE Peripheral"
    };
    [self.peripheralManager startAdvertising:advertisementData];
}

这里通过 startAdvertising: 方法开始广播,广播数据中包含了服务 UUID 和本地设备名称。

处理中心设备连接

当有中心设备连接到周边设备时,会调用代理方法 peripheralManager:didConnectPeripheral:

- (void)peripheralManager:(CBPeripheralManager *)peripheral didConnectPeripheral:(CBPeripheral *)central {
    NSLog(@"Connected to central: %@", central.name);
}

同样,当连接断开时,会调用 peripheralManager:didDisconnectPeripheral:error: 代理方法。

- (void)peripheralManager:(CBPeripheralManager *)peripheral didDisconnectPeripheral:(CBPeripheral *)central error:(NSError *)error {
    NSLog(@"Disconnected from central: %@, error: %@", central.name, error);
}

处理中心设备对特征的读取与写入

  1. 处理读取请求:当中心设备请求读取特征值时,会调用 peripheralManager:didReceiveReadRequest:forCharacteristic: 代理方法。
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request forCharacteristic:(CBCharacteristic *)characteristic {
    if (request.offset > characteristic.value.length) {
        request.error = [NSError errorWithDomain:CBATTErrorDomain code:CBATTErrorInvalidOffset userInfo:nil];
        [self.peripheralManager respondToRequest:request withResult:CBATTErrorInvalidOffset];
        return;
    }
    NSData *subdata = [characteristic.value subdataWithRange:NSMakeRange(request.offset, characteristic.value.length - request.offset)];
    request.value = subdata;
    [self.peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
}

这里首先检查请求的偏移量是否有效,如果无效则返回错误。如果有效,则根据偏移量获取相应的数据,并返回成功响应。

  1. 处理写入请求:当中心设备请求写入特征值时,会调用 peripheralManager:didReceiveWriteRequests: 代理方法。
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests {
    for (CBATTRequest *request in requests) {
        if (request.characteristic.properties & CBCharacteristicPropertyWrite) {
            // 更新特征值
            request.characteristic.value = request.value;
            [self.peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
        } else {
            [self.peripheralManager respondToRequest:request withResult:CBATTErrorWriteNotPermitted];
        }
    }
}

这里遍历所有的写入请求,首先检查特征是否支持写入操作。如果支持,则更新特征值并返回成功响应;如果不支持,则返回写入不允许的错误响应。

向中心设备发送通知

周边设备可以主动向已连接的中心设备发送通知。首先要确保中心设备已经订阅了通知。然后通过 updateValue:forCharacteristic:onSubscribedCentrals: 方法发送通知。

NSData *newData = [@"New data for notification" dataUsingEncoding:NSUTF8StringEncoding];
[self.peripheralManager updateValue:newData forCharacteristic:myCharacteristic onSubscribedCentrals:nil];

这里假设 myCharacteristic 是需要发送通知的特征,newData 是要发送的数据。

常见问题与解决方法

连接失败问题

  1. 原因分析:可能是设备未开启蓝牙、设备不支持 BLE、应用未获得授权等。
  2. 解决方法:在 centralManagerDidUpdateState: 方法中,根据不同的状态进行提示或处理。例如,如果是 CBCentralManagerStateUnauthorized 状态,可以引导用户去设置中开启应用的蓝牙权限。

数据解析问题

  1. 原因分析:BLE 设备返回的数据格式可能多种多样,如二进制、十六进制等,解析不当就会出错。
  2. 解决方法:在处理数据前,要清楚设备返回的数据格式。例如,如果是二进制数据表示温度,需要根据数据长度和编码方式将其转换为实际的温度值。可以使用 NSData 的相关方法,如 getBytes:length: 来获取字节数据并进行解析。

广播问题

  1. 原因分析:广播数据设置不正确、周边管理器状态异常等都可能导致广播失败。
  2. 解决方法:在 peripheralManagerDidUpdateState: 方法中确保周边管理器状态为 CBPeripheralManagerStatePoweredOn 时才开始广播。检查广播数据的格式和内容,确保包含了正确的服务 UUID 和其他必要信息。

性能优化

减少扫描时间

  1. 策略:在发现目标设备后,及时停止扫描。例如,在 centralManager:didDiscoverPeripheral:advertisementData:RSSI: 方法中,一旦发现目标设备,就调用 [self.centralManager stopScan] 方法。
  2. 原理:持续扫描会消耗设备的电量和系统资源,及时停止扫描可以减少这些消耗。

合理设置通知频率

  1. 策略:对于一些变化频繁的数据特征,根据实际需求合理设置通知频率。如果数据变化不频繁,可以适当降低通知频率。
  2. 原理:过高的通知频率会增加设备之间的数据传输量,消耗电量和带宽。合理设置通知频率可以在保证数据及时性的同时,降低资源消耗。

优化数据处理

  1. 策略:在处理大量 BLE 数据时,采用高效的数据解析和存储方法。例如,对于连续的传感器数据,可以采用队列或环形缓冲区来存储,避免频繁的内存分配和释放。
  2. 原理:高效的数据处理方法可以提高程序的运行效率,减少卡顿现象,提升用户体验。

通过以上对 Objective-C 中 Core Bluetooth 蓝牙通信的详细介绍,从框架概述、中心与周边模式开发流程,到常见问题解决和性能优化,开发者可以全面掌握 Core Bluetooth 在 Objective-C 项目中的应用,开发出稳定、高效的 BLE 通信应用程序。无论是开发智能家居控制、健康监测等各类 BLE 相关应用,都能以此为基础进行深入开发。