iOS - 后台BLE扫描随机冻结

16

更新14/08 - 3 - 找到真正的解决方案:

您可以在下面的答案中查看解决方案!

更新16/06 - 2 - 可能是解决方案:

正如Sandy Chapman在他的回答评论中建议的那样,现在我能够通过使用这种方法在扫描开始时检索到我的外围设备:

- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers

我正在尝试通过在扫描开始时获取我的外围设备并启动连接(即使它不在范围内)来使其工作。iOS会保持其活动状态,直到找到我正在寻找的设备。

请注意,如果后台有使用蓝牙的发布版本应用程序,可能会导致iOS 8.x中存在错误,从时间到时间中断扫描(消失回调)。

更新于16/06:

所以我使用retrievePeripheralsWithServices检查是否有设备在我开始扫描时已连接。当我遇到此错误时,我启动应用程序并首先要做的事情是

- (void) applicationDidBecomeActive:(UIApplication *)application

问题是要检查返回数组的大小。每次出现错误时,它总是为0。如果我的设备在当前运行中没有建立任何连接,也会出现错误。当我用另一个设备触发命令时,我也能够看到我的设备广告,但在另一个设备中出现错误。

更新10/06:

  • 我让我的应用程序整晚运行,以检查是否有任何内存泄漏或大量资源使用情况,在后台运行约12-14个小时后,这是我的结果。内存/CPU使用情况与我离开时完全相同。这使我认为我的应用程序没有任何泄漏,可能导致iOS关闭它以获得内存/CPU使用情况。

Resource usage analysis

更新08/06:

  • 请注意,这不是广告问题,因为我们的BLE设备持续供电,并且我们使用了我们可以找到的最强的BLE电子卡。
  • 这也不是后台iOS检测时间的问题。我等待很长时间(20〜30分钟)来确保不是这个问题。

原始问题:

我目前正在开发一个处理与BLE设备通信的应用程序。我的一个限制条件是,只有在发送命令或读取数据时才必须连接到此设备。完成后,我必须尽快断开连接,以允许其他潜在用户执行相同的操作。

应用程序的一个功能如下:

  • 当应用程序处于后台时,用户可以启用自动命令。如果在10分钟内未检测到该设备,则触发此自动命令。
  • 我的应用程序一直扫描直到找到我的BLE设备。
  • 为了在需要时保持其唤醒状态,我每次都重新开始扫描,因为忽略CBCentralManagerScanOptionAllowDuplicatesKey选项。
  • 当检测到它时,我会检查上次检测是否超过10分钟。如果是这种情况,我将连接到设备,然后写入对应所需服务的特征。

目标是在用户进入范围时触发此设备。这可能会在离开范围几分钟或几个小时后发生,这取决于我的用户习惯。

一切运行良好,但有时(似乎随机时间),扫描会“冻结”。我的处理程序已成功完成,但是经过几次后,我的应用程序正在扫描,但我的didDiscoverPeripheral:回调从未被调用,即使我的测试设备就在我的BLE设备面前。有时可能需要一段时间才能检测到它,但是在这里,几分钟后什么都没有发生。

我认为 iOS 可能会在内存不足时杀死我的应用程序,但是当我关闭和打开蓝牙时,"centralManagerDidUpdateState:" 将被正确调用。如果我的应用程序被系统强制关闭了,这种情况就不应该发生,对吗?
如果我打开我的应用程序,扫描将重新启动并恢复运行。我还检查了 iOS 是否在 180 秒的活动之后关闭我的应用程序,但实际上并不是这样,因为它在此时间后仍然正常工作。
我已经设置了我的 .plist 文件以具有正确的设置(在 "UIBackgroundModes" 中的 "bluetooth-central")。管理所有 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];
}
  }
3个回答

3
在您的示例代码中,似乎没有调用以下这些方法之一:
- (NSArray<CBPeripheral *> * nonnull)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> * nonnull)serviceUUIDs
- (NSArray<CBPeripheral *> * nonnull)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> * nonnull)identifiers
这可能是因为您处于等待状态,并且不会收到didDiscoverPeripheral 回调,因为系统已经连接此外设。在实例化 CBCentralManager 后,请首先调用其中一个方法来检查外设是否已连接。
使用状态恢复的 central manager 的启动流程如下:
  1. 从恢复状态中检查可用的外设(在您的情况下不需要)。
  2. 检查以前扫描发现的外设(我将它们保存在可用外设的数组中)。
  3. 检查已连接的外设(使用上述方法)。
  4. 如果上述都没有返回外设,则开始扫描。
还有一件事需要注意:iOS 将缓存外设广告的特征和 UUID。如果这些内容更改了,清除缓存的唯一方法是在 iOS 系统设置中切换蓝牙开关。

你好,可能是这样的情况。在我的过程中,每个命令后我都会断开与外围设备的连接。但是我的管理类非常复杂,所以我可能会漏掉一些东西。我会尝试您的解决方案,并将及时告知结果(我会在“赏金计时器”结束之前尝试完成)。 - Jissay
还要注意的是,我也将检测到的外围设备存储在一个数组中。每次开始新的扫描(大约每8秒钟)时,我都会初始化这个数组。 - Jissay
所以我使用retrievePeripheralsWithServices进行了检查。当我遇到错误时,我启动应用程序,并在- (void) applicationDidBecomeActive:(UIApplication *)application中的第一件事是检查返回数组的大小。每次我遇到错误时,它总是为0 :/ 如果我的设备在当前运行中之前没有进行任何连接,也可能会出现错误。我将尝试在扫描开始后立即进行检查。 - Jissay
@Jissay:只是为了确认这绝对不是你的外围设备,当您在第二个设备上启动应用程序并尝试连接外围设备时会发生什么?它能够连接吗?如果不能,则可能是外围设备上的广告没有重新启动。 - Sandy Chapman
1
@Jissay,你可以尝试使用retrievePeripheralsWithIdentifiers:方法吗?基本上,该方法应返回任何先前连接的设备,然后即使您尚未收到广告,您也应该能够启动与该设备的连接。 - Sandy Chapman
显示剩余5条评论

3

经过多次尝试,我可能终于找到了真正的解决方案。

值得注意的是,我与一位苹果工程师通过技术支持进行了多次交流。他引导我以两种方式进行:

  • 检查正确实现核心蓝牙保留/恢复状态。您可以在 Stack Overflow 上找到一些相关线程,如此线程这个。在此处,您还可以找到苹果的文档:Core Bluetooth 背景处理
  • 检查我蓝牙设备中实现的连接参数,例如: 连接间隔最小和最大连接间隔从机延迟连接超时。在苹果蓝牙设计指南中有很多信息。

真正的解决方案可能涉及这些元素。在寻找解决此问题的过程中,我添加和更正了一些内容。但是使其工作的关键(我测试了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类)添加了以下代码行:
@property (atomic, strong) dispatch_queue_t bluetoothQueue;

正如您所见,它有点混乱 :)

现在我可以扫描需要的时间。感谢您的帮助!希望有一天能对某人有所帮助。


0

这可能与您尝试配对的蓝牙设备有关,其中一些设备具有较低的广告速率,通常是为了节省电池寿命。我首先要检查的是BLE设备配置广告的频率。

这甚至可能与iOS设备不像其他设备那样频繁扫描有关,如this post所述。


我们的设备持续供电(因此我们没有任何电池问题需要处理),我们使用了最好的卡片来避免这种问题。我已经看到了你提到的帖子,并为了测试它,我等了很长时间(20-30分钟)以查看我的设备是否会被扫描检测到。答案是否定的。我同意iOS可能非常缓慢地检测到BLE设备,但20分钟似乎太高了,不太可能是真的! - Jissay
你确认你使用的iOS版本中没有蓝牙问题吗?在追求节能的过程中,有时会破坏BLE/WiFi等功能。我记得最近在iOS8中读到了类似的内容。找到一个链接:http://www.ibtimes.co.uk/ios-8-3-users-facing-issues-bluetooth-gps-receivers-1496526 - cjnevin
我已经测试了许多iOS 8版本(8.1、8.2、8.3),但我还没有在iOS 7+上测试过(以确认它是否是iOS 8及以上版本的错误)。一旦我拥有一个iOS 7设备,我就能够告诉并更新我的问题(可能明天)。我会以这种方式进行调查。 - Jissay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接