iOS - 后台 BLE 扫描随机冻结
iOS - BLE Scanning on background freezes randomly
更新 14/08 - 3 - 找到真正的解决方案:
您可以在下面的答案中查看解决方案!
更新 16/06 - 2 - 可能是解决方案:
正如 Sandy Chapman 在其回答的评论中所建议的那样,我现在可以使用此方法在扫描开始时检索我的外围设备:
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
我实际上是在尝试通过在扫描开始时恢复外围设备并在需要时启动连接(即使它不在范围内)来使其正常工作。 iOS 将使其保持活动状态,直到找到我正在寻找的设备。
另请注意,iOS 8.x 中可能存在一个错误,使具有调试版本的应用不时扫描(消失的回调)如果后台有另一个应用程序正在使用蓝牙发布版本。
2006 年 16 月更新:
所以我检查了 retrievePeripheralsWithServices 在我开始扫描时是否连接了任何设备。
当我遇到错误时,我会启动该应用程序,然后我在
中做的第一件事
- (void) applicationDidBecomeActive:(UIApplication *)application
是检查返回数组的大小。每次我遇到错误时,它始终为 0。如果我的设备在当前 运行 之前没有建立任何连接,也会发生该错误。当我在另一台设备上遇到错误时,我还能够看到我的设备广告并通过第二台设备触发命令。
2006 年 10 月更新:
- 我整个晚上都离开了我的应用程序 运行,以检查是否没有任何内存泄漏或大量资源使用,这是 运行 大约 12-14 小时后的结果在后台。 Memory/CPU 用法和我离开时完全一样。这让我认为我的应用程序没有任何可能导致 iOS 关闭它以恢复 memory/CPU 使用的泄漏。
更新 08/06:
- 请注意,这不是广告问题,因为我们的 BLE 设备一直供电,而且我们使用了我们能找到的最强大的 BLE 电子卡。
- 这也不是后台 iOS 检测时间的问题。我等了很长时间(20~30 分钟)以确保不是这个问题。
原始问题
我目前正在开发一个处理与 BLE 设备通信的应用程序。我的限制之一是,只有当我必须发送命令或读取数据时,我才必须连接到该设备。完成后我必须尽快断开连接,以允许其他潜在用户也这样做。
该应用程序的其中一项功能如下:
- 用户可以在应用程序处于后台时启用自动命令。如果在 10 分钟内未检测到设备,则会触发此自动命令。
- 我的应用会扫描直到找到我的 BLE 设备。
- 为了在我需要它的时候让它保持清醒,我每次都重新开始扫描,因为 CBCentralManagerScanOptionAllowDuplicatesKey 选项无知。
- 检测到时,我正在检查上次检测是否在 10 分钟前。如果是这样,我连接到设备,然后写入我需要的服务对应的特性。
目标是当用户进入范围时触发此设备。可能在超出范围后几分钟,比如几个小时后,这取决于我的用户习惯。
这种方式一切正常,但有时(似乎是随机发生的)扫描排序 "freezes"。我的过程完成得很好,但几次后,我看到我的应用程序正在扫描,但我的 didDiscoverPeripheral: 回调从未被调用,即使我的测试设备就在我的 BLE 设备前面。有时可能需要一段时间才能检测到它,但在这里,几分钟后什么也没有发生。
我在想 iOS 可能杀死了我的应用程序以收回内存,但是当我关闭并打开蓝牙时,centralManagerDidUpdateState: 被调用为正确的方式.如果我的应用程序被杀死,情况不应该是这样吗?
如果我打开我的应用程序,扫描将重新启动并且它会恢复生机。
我还检查了 iOS 在 activity 180 秒后没有关闭我的应用程序,但事实并非如此,因为它在这段时间后运行良好。
我已经将我的 .plist 设置为具有正确的设置(bluetooth-central in UIBackgroundModes)。我的 class 管理所有 BLE 处理存储在我的 AppDelegate 中作为可通过我的所有应用程序访问的单例。我还测试过切换创建此对象的位置。目前我正在 application:didFinishLaunchingWithOptions: 方法中创建它。我试图把它放在我的 AppDelegate init: 中,但是如果我这样做,每次在后台时扫描都会失败。
我不知道我可以向您展示我的代码的哪一部分,以帮助您更好地理解我的过程。以下是一些可能有帮助的示例。请注意,“AT_appDelegate”是一个宏,用于访问我的 AppDelegate.
// Init of my DeviceManager class that handles all BLE processing
- (id) init {
self = [super init];
// Flags creation
self.autoConnectTriggered = NO;
self.isDeviceReady = NO;
self.connectionUncomplete = NO;
self.currentCommand = NONE;
self.currentCommand_index = 0;
self.signalOkDetectionCount = 0; // Helps to find out if device is at a good range or too far
self.connectionFailedCount = 0; // Helps in a "try again" process if a command fails
self.main_uuid = [CBUUID UUIDWithString:MAINSERVICE_UUID];
self.peripheralsRetainer = [[NSMutableArray alloc] init];
self.lastDeviceDetection = nil;
// Ble items creation
dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue];
[self startScanning];
return self;
}
// The way i start the scan
- (void) startScanning {
if (!self.isScanning && self.centralManager.state == CBCentralManagerStatePoweredOn) {
CLS_LOG(@"### Start scanning ###");
self.isScanning = YES;
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:!self.isBackground] forKey:CBCentralManagerScanOptionAllowDuplicatesKey];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
}
}
// The way i stop and restart the scan after i've found our device. Contains some of foreground (UI update) process that you can ignore
- (void) stopScanningAndRestart: (BOOL) restart {
CLS_LOG(@"### Scanning terminated ###");
if (self.isScanning) {
self.isScanning = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager stopScan];
});
// Avoid clearing the connection when waiting for notification (remote + learning)
if (!self.isWaitingNotifiy && !self.isSynchronizing && self.currentCommand == NONE ) {
// If no device found during scan, update view
if (self.deviceToReach == nil && !self.isBackground) {
// Check if any connected devices last
if (![self isDeviceStillConnected]) {
CLS_LOG(@"--- Device unreachable for view ---");
} else {
self.isDeviceInRange = YES;
self.deviceToReach = AT_appDelegate.user.device.blePeripheral;
}
[self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
}
// Reset var
self.deviceToReach = nil;
self.isDeviceInRange = NO;
self.signalOkDetectionCount = 0;
// Check if autotrigger needs to be done again - If time interval is higher enough,
// reset autoConnectTriggered to NO. If user has been away for <AUTOTRIGGER_INTERVAL>
// from the device, it will trigger again next time it will be detected.
if ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL) {
CLS_LOG(@"### Auto trigger is enabled ###");
self.autoConnectTriggered = NO;
}
}
}
if (restart) {
[self startScanning];
}
}
// Here is my detection process, the flag "isInBackground" is set up each time the app goes background
- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
CLS_LOG(@"### : %@ -- %@", peripheral.name, RSSI);
BOOL deviceAlreadyShown = [AT_appDelegate isDeviceAvailable];
// If current device has no UUID set, check if peripheral is the right one
// with its name, containing his serial number (macaddress) returned by
// the server on remote adding
NSString *p1 = [[[peripheral.name stringByReplacingOccurrencesOfString:@":" withString:@""] stringByReplacingOccurrencesOfString:@"Extel " withString:@""] uppercaseString];
NSString *p2 = [AT_appDelegate.user.device.serial uppercaseString];
if ([p1 isEqualToString:p2]) {
AT_appDelegate.user.device.scanUUID = peripheral.identifier;
}
// Filter peripheral connection with uuid
if ([AT_appDelegate.user.device.scanUUID isEqual:peripheral.identifier]) {
if (([RSSI intValue] > REQUIRED_SIGNAL_STRENGTH && [RSSI intValue] < 0) || self.isBackground) {
self.signalOkDetectionCount++;
self.deviceToReach = peripheral;
self.isDeviceInRange = (self.signalOkDetectionCount >= REQUIRED_SIGNAL_OK_DETECTIONS);
[peripheral setDelegate:self];
// Reset blePeripheral if daughter board has been switched and there were
// not enough time for the software to notice connection has been lost.
// If that was the case, the device.blePeripheral has not been reset to nil,
// and might be different than the new peripheral (from the new daugtherboard)
if (AT_appDelegate.user.device.blePeripheral != nil) {
if (![AT_appDelegate.user.device.blePeripheral.name isEqualToString:peripheral.name]) {
AT_appDelegate.user.device.blePeripheral = nil;
}
}
if (self.lastDeviceDetection == nil ||
([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL)) {
self.autoConnectTriggered = NO;
}
[peripheral readRSSI];
AT_appDelegate.user.device.blePeripheral = peripheral;
self.lastDeviceDetection = [NSDate date];
if (AT_appDelegate.user.device.autoconnect) {
if (!self.autoConnectTriggered && !self.autoTriggerConnectionLaunched) {
CLS_LOG(@"--- Perform trigger ! ---");
self.autoTriggerConnectionLaunched = YES;
[self executeCommand:W_TRIGGER onDevice:AT_appDelegate.user.device]; // trigger !
return;
}
}
}
if (deviceAlreadyShown) {
[self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
}
}
if (self.isBackground && AT_appDelegate.user.device.autoconnect) {
CLS_LOG(@"### Relaunch scan ###");
[self stopScanningAndRestart:YES];
}
}
这可能与您尝试配对的蓝牙设备有关,其中一些设备的广告率较低,这通常是为了节省电池寿命。我要检查的第一件事是 BLE 设备配置为广播的频率。
这甚至可能与 iOS 设备不像其他设备那样频繁扫描有关,如 this post 中所述。
在您的示例代码中,您似乎没有在 CBCentralManager 上调用这些方法中的任何一个:
- (NSArray<CBPeripheral *> * nonnull)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> * nonnull)serviceUUIDs
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
您可能正处于等待 didDiscoverPeripheral
永远不会出现的状态,因为系统已经连接。实例化您的 CBCentralManager 后,首先通过调用这些方法之一检查您的外设是否已连接。
我的中央管理器使用状态恢复的启动流程如下:
- 从恢复状态检查可用外围设备(您的情况不需要)。
- 检查从之前的扫描中发现的外围设备(我将它们保存在可用外围设备的数组中)。
- 检查连接的外围设备(使用上述方法)。
- 如果上述 none 返回外围设备,则开始扫描。
您可能已经发现的另一个提示:iOS 将缓存外围设备公布的特征和 UUID。如果这些发生变化,清除缓存的唯一方法是在 iOS 系统设置中关闭和打开蓝牙。
经过多次尝试,我可能终于找到了真正的解决方案。
仅供参考,我与 Apple 的一位工程师进行了多次交谈(通过技术支持)。他以两种方式引导我:
- 检查核心蓝牙保存/恢复状态是否正确实现。您将能够在堆栈溢出上找到一些关于它的线程,例如 this thread or this one. You will also found apple documentation useful here : Core Bluetooth Background Processing.
- 正在检查我的蓝牙设备中实现的连接参数,例如:连接间隔、最小和最大连接间隔、从延迟 和连接超时。 Apple Bluetooth Design Guidelines
中有很多信息
真正的解决方案可能会参与这些要素。我在搜索以清除此错误时添加并更正了一些内容。但是让它工作的东西(我测试了大约 4-5 小时,它根本没有冻结)大约是 dispatch_queue.
我之前做了什么:
// Initialisation
dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];
// Start scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
// Stop scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager stopScan];
});
我现在在做什么:
// Initialisation
self.bluetoothQueue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:self.bluetoothQueue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];
// Start scanning
dispatch_async(self.bluetoothQueue, ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
// Stop scanning
dispatch_async(self.bluetoothQueue, ^{
[self.centralManager stopScan];
});
请注意,我将此行添加到我的 DeviceManager.h(我的主要 Ble class):
@property (atomic, strong) dispatch_queue_t bluetoothQueue;
如您所见,它有点乱:)
所以现在我可以根据需要进行扫描。谢谢你的帮助 !我希望有一天它能对某人有所帮助。
更新 14/08 - 3 - 找到真正的解决方案:
您可以在下面的答案中查看解决方案!
更新 16/06 - 2 - 可能是解决方案:
正如 Sandy Chapman 在其回答的评论中所建议的那样,我现在可以使用此方法在扫描开始时检索我的外围设备:
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
我实际上是在尝试通过在扫描开始时恢复外围设备并在需要时启动连接(即使它不在范围内)来使其正常工作。 iOS 将使其保持活动状态,直到找到我正在寻找的设备。
另请注意,iOS 8.x 中可能存在一个错误,使具有调试版本的应用不时扫描(消失的回调)如果后台有另一个应用程序正在使用蓝牙发布版本。
2006 年 16 月更新:
所以我检查了 retrievePeripheralsWithServices 在我开始扫描时是否连接了任何设备。 当我遇到错误时,我会启动该应用程序,然后我在
中做的第一件事- (void) applicationDidBecomeActive:(UIApplication *)application
是检查返回数组的大小。每次我遇到错误时,它始终为 0。如果我的设备在当前 运行 之前没有建立任何连接,也会发生该错误。当我在另一台设备上遇到错误时,我还能够看到我的设备广告并通过第二台设备触发命令。
2006 年 10 月更新:
- 我整个晚上都离开了我的应用程序 运行,以检查是否没有任何内存泄漏或大量资源使用,这是 运行 大约 12-14 小时后的结果在后台。 Memory/CPU 用法和我离开时完全一样。这让我认为我的应用程序没有任何可能导致 iOS 关闭它以恢复 memory/CPU 使用的泄漏。
更新 08/06:
- 请注意,这不是广告问题,因为我们的 BLE 设备一直供电,而且我们使用了我们能找到的最强大的 BLE 电子卡。
- 这也不是后台 iOS 检测时间的问题。我等了很长时间(20~30 分钟)以确保不是这个问题。
原始问题
我目前正在开发一个处理与 BLE 设备通信的应用程序。我的限制之一是,只有当我必须发送命令或读取数据时,我才必须连接到该设备。完成后我必须尽快断开连接,以允许其他潜在用户也这样做。
该应用程序的其中一项功能如下:
- 用户可以在应用程序处于后台时启用自动命令。如果在 10 分钟内未检测到设备,则会触发此自动命令。
- 我的应用会扫描直到找到我的 BLE 设备。
- 为了在我需要它的时候让它保持清醒,我每次都重新开始扫描,因为 CBCentralManagerScanOptionAllowDuplicatesKey 选项无知。
- 检测到时,我正在检查上次检测是否在 10 分钟前。如果是这样,我连接到设备,然后写入我需要的服务对应的特性。
目标是当用户进入范围时触发此设备。可能在超出范围后几分钟,比如几个小时后,这取决于我的用户习惯。
这种方式一切正常,但有时(似乎是随机发生的)扫描排序 "freezes"。我的过程完成得很好,但几次后,我看到我的应用程序正在扫描,但我的 didDiscoverPeripheral: 回调从未被调用,即使我的测试设备就在我的 BLE 设备前面。有时可能需要一段时间才能检测到它,但在这里,几分钟后什么也没有发生。
我在想 iOS 可能杀死了我的应用程序以收回内存,但是当我关闭并打开蓝牙时,centralManagerDidUpdateState: 被调用为正确的方式.如果我的应用程序被杀死,情况不应该是这样吗? 如果我打开我的应用程序,扫描将重新启动并且它会恢复生机。 我还检查了 iOS 在 activity 180 秒后没有关闭我的应用程序,但事实并非如此,因为它在这段时间后运行良好。
我已经将我的 .plist 设置为具有正确的设置(bluetooth-central in UIBackgroundModes)。我的 class 管理所有 BLE 处理存储在我的 AppDelegate 中作为可通过我的所有应用程序访问的单例。我还测试过切换创建此对象的位置。目前我正在 application:didFinishLaunchingWithOptions: 方法中创建它。我试图把它放在我的 AppDelegate init: 中,但是如果我这样做,每次在后台时扫描都会失败。
我不知道我可以向您展示我的代码的哪一部分,以帮助您更好地理解我的过程。以下是一些可能有帮助的示例。请注意,“AT_appDelegate”是一个宏,用于访问我的 AppDelegate.
// Init of my DeviceManager class that handles all BLE processing
- (id) init {
self = [super init];
// Flags creation
self.autoConnectTriggered = NO;
self.isDeviceReady = NO;
self.connectionUncomplete = NO;
self.currentCommand = NONE;
self.currentCommand_index = 0;
self.signalOkDetectionCount = 0; // Helps to find out if device is at a good range or too far
self.connectionFailedCount = 0; // Helps in a "try again" process if a command fails
self.main_uuid = [CBUUID UUIDWithString:MAINSERVICE_UUID];
self.peripheralsRetainer = [[NSMutableArray alloc] init];
self.lastDeviceDetection = nil;
// Ble items creation
dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue];
[self startScanning];
return self;
}
// The way i start the scan
- (void) startScanning {
if (!self.isScanning && self.centralManager.state == CBCentralManagerStatePoweredOn) {
CLS_LOG(@"### Start scanning ###");
self.isScanning = YES;
NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:!self.isBackground] forKey:CBCentralManagerScanOptionAllowDuplicatesKey];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
}
}
// The way i stop and restart the scan after i've found our device. Contains some of foreground (UI update) process that you can ignore
- (void) stopScanningAndRestart: (BOOL) restart {
CLS_LOG(@"### Scanning terminated ###");
if (self.isScanning) {
self.isScanning = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager stopScan];
});
// Avoid clearing the connection when waiting for notification (remote + learning)
if (!self.isWaitingNotifiy && !self.isSynchronizing && self.currentCommand == NONE ) {
// If no device found during scan, update view
if (self.deviceToReach == nil && !self.isBackground) {
// Check if any connected devices last
if (![self isDeviceStillConnected]) {
CLS_LOG(@"--- Device unreachable for view ---");
} else {
self.isDeviceInRange = YES;
self.deviceToReach = AT_appDelegate.user.device.blePeripheral;
}
[self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
}
// Reset var
self.deviceToReach = nil;
self.isDeviceInRange = NO;
self.signalOkDetectionCount = 0;
// Check if autotrigger needs to be done again - If time interval is higher enough,
// reset autoConnectTriggered to NO. If user has been away for <AUTOTRIGGER_INTERVAL>
// from the device, it will trigger again next time it will be detected.
if ([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL) {
CLS_LOG(@"### Auto trigger is enabled ###");
self.autoConnectTriggered = NO;
}
}
}
if (restart) {
[self startScanning];
}
}
// Here is my detection process, the flag "isInBackground" is set up each time the app goes background
- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
CLS_LOG(@"### : %@ -- %@", peripheral.name, RSSI);
BOOL deviceAlreadyShown = [AT_appDelegate isDeviceAvailable];
// If current device has no UUID set, check if peripheral is the right one
// with its name, containing his serial number (macaddress) returned by
// the server on remote adding
NSString *p1 = [[[peripheral.name stringByReplacingOccurrencesOfString:@":" withString:@""] stringByReplacingOccurrencesOfString:@"Extel " withString:@""] uppercaseString];
NSString *p2 = [AT_appDelegate.user.device.serial uppercaseString];
if ([p1 isEqualToString:p2]) {
AT_appDelegate.user.device.scanUUID = peripheral.identifier;
}
// Filter peripheral connection with uuid
if ([AT_appDelegate.user.device.scanUUID isEqual:peripheral.identifier]) {
if (([RSSI intValue] > REQUIRED_SIGNAL_STRENGTH && [RSSI intValue] < 0) || self.isBackground) {
self.signalOkDetectionCount++;
self.deviceToReach = peripheral;
self.isDeviceInRange = (self.signalOkDetectionCount >= REQUIRED_SIGNAL_OK_DETECTIONS);
[peripheral setDelegate:self];
// Reset blePeripheral if daughter board has been switched and there were
// not enough time for the software to notice connection has been lost.
// If that was the case, the device.blePeripheral has not been reset to nil,
// and might be different than the new peripheral (from the new daugtherboard)
if (AT_appDelegate.user.device.blePeripheral != nil) {
if (![AT_appDelegate.user.device.blePeripheral.name isEqualToString:peripheral.name]) {
AT_appDelegate.user.device.blePeripheral = nil;
}
}
if (self.lastDeviceDetection == nil ||
([[NSDate date] timeIntervalSinceReferenceDate] - [self.lastDeviceDetection timeIntervalSinceReferenceDate] > AUTOTRIGGER_INTERVAL)) {
self.autoConnectTriggered = NO;
}
[peripheral readRSSI];
AT_appDelegate.user.device.blePeripheral = peripheral;
self.lastDeviceDetection = [NSDate date];
if (AT_appDelegate.user.device.autoconnect) {
if (!self.autoConnectTriggered && !self.autoTriggerConnectionLaunched) {
CLS_LOG(@"--- Perform trigger ! ---");
self.autoTriggerConnectionLaunched = YES;
[self executeCommand:W_TRIGGER onDevice:AT_appDelegate.user.device]; // trigger !
return;
}
}
}
if (deviceAlreadyShown) {
[self.delegate performSelectorOnMainThread:@selector(updateView) withObject:nil waitUntilDone:YES];
}
}
if (self.isBackground && AT_appDelegate.user.device.autoconnect) {
CLS_LOG(@"### Relaunch scan ###");
[self stopScanningAndRestart:YES];
}
}
这可能与您尝试配对的蓝牙设备有关,其中一些设备的广告率较低,这通常是为了节省电池寿命。我要检查的第一件事是 BLE 设备配置为广播的频率。
这甚至可能与 iOS 设备不像其他设备那样频繁扫描有关,如 this post 中所述。
在您的示例代码中,您似乎没有在 CBCentralManager 上调用这些方法中的任何一个:
- (NSArray<CBPeripheral *> * nonnull)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> * nonnull)serviceUUIDs
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
您可能正处于等待 didDiscoverPeripheral
永远不会出现的状态,因为系统已经连接。实例化您的 CBCentralManager 后,首先通过调用这些方法之一检查您的外设是否已连接。
我的中央管理器使用状态恢复的启动流程如下:
- 从恢复状态检查可用外围设备(您的情况不需要)。
- 检查从之前的扫描中发现的外围设备(我将它们保存在可用外围设备的数组中)。
- 检查连接的外围设备(使用上述方法)。
- 如果上述 none 返回外围设备,则开始扫描。
您可能已经发现的另一个提示:iOS 将缓存外围设备公布的特征和 UUID。如果这些发生变化,清除缓存的唯一方法是在 iOS 系统设置中关闭和打开蓝牙。
经过多次尝试,我可能终于找到了真正的解决方案。
仅供参考,我与 Apple 的一位工程师进行了多次交谈(通过技术支持)。他以两种方式引导我:
- 检查核心蓝牙保存/恢复状态是否正确实现。您将能够在堆栈溢出上找到一些关于它的线程,例如 this thread or this one. You will also found apple documentation useful here : Core Bluetooth Background Processing.
- 正在检查我的蓝牙设备中实现的连接参数,例如:连接间隔、最小和最大连接间隔、从延迟 和连接超时。 Apple Bluetooth Design Guidelines 中有很多信息
真正的解决方案可能会参与这些要素。我在搜索以清除此错误时添加并更正了一些内容。但是让它工作的东西(我测试了大约 4-5 小时,它根本没有冻结)大约是 dispatch_queue.
我之前做了什么:
// Initialisation
dispatch_queue_t queue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:queue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];
// Start scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
// Stop scanning
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self.centralManager stopScan];
});
我现在在做什么:
// Initialisation
self.bluetoothQueue = dispatch_queue_create("com.onset.corebluetooth.queue", DISPATCH_QUEUE_SERIAL);
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:self.bluetoothQueue options:@{CBCentralManagerOptionRestoreIdentifierKey:RESTORE_KEY}];
// Start scanning
dispatch_async(self.bluetoothQueue, ^{
[self.centralManager scanForPeripheralsWithServices:@[self.main_uuid] options:options];
});
// Stop scanning
dispatch_async(self.bluetoothQueue, ^{
[self.centralManager stopScan];
});
请注意,我将此行添加到我的 DeviceManager.h(我的主要 Ble class):
@property (atomic, strong) dispatch_queue_t bluetoothQueue;
如您所见,它有点乱:)
所以现在我可以根据需要进行扫描。谢谢你的帮助 !我希望有一天它能对某人有所帮助。