用于应用程序后台处理的 iOS CoreBluetooth 调度队列

Mar*_*arc 5 grand-central-dispatch ios core-bluetooth ios-bluetooth dispatch-queue

首先的问题是,在核心角色中使用核心蓝牙将数据发送到蓝牙 LE 设备的最佳方式是什么。需要处理数据,如果它在 UI 线程上运行,则需要足够的时间在 UI 线程上引起问题。用户将在手机应用程序打开的情况下启动该过程,然后继续使用该应用程序或关闭该应用程序并期望数据继续发送到设备。

我发现了 2 种非常糟糕的方法,它们似乎有效

  • 将蓝牙 CBCentralManager 对象放在主队列上并冒着阻塞 UI 的风险
  • 忽略来自 iOS 蓝牙堆栈的指示,表明它尚未准备好传输并有丢失数据的风险。

这似乎源于 iOS 线程/调度队列以及 iOS 蓝牙内部结构。

蓝牙 LE iOS 应用程序作为核心角色连接到蓝牙 LE 设备。CBCentralManager 是根据苹果文档初始化的。队列定义为:

用于调度中心角色事件的调度队列。如果值为 nil,则中央管理器使用主队列分派中央角色事件。

正如vladiulianbogdan 对 Swift 的回答所建议的:为蓝牙中央管理器选择队列,我们应该为 CBCentralManager 创建一个串行队列。这似乎是有道理的,有一段时间我一直在遵循这个建议。此外allprog评论斯威夫特CoreBluetooth:在一个单独的线程应该CentralManager运行 表明,主队列将暂停,但其他队列不会,这是我所看到的是相反的。

在为蓝牙使用串行队列时,最好使用与主线程不同的队列。有一个问题:回调:

    -(void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral *)peripheral
    {
        [self sendNextBluetoothLePacket];
    }
Run Code Online (Sandbox Code Playgroud)

停止被调用。还有另一种方法可以检查外设是否准备好发送更多数据,CBPeripheral 有一个成员变量 canSendWriteWithoutResponse,如果可以发送则返回 true。此变量也开始重新调整 false 并且永远不会回到 true。

我从Sandeep Bhandari找到了这条评论,它说当应用程序进入后台时,所有队列线程都会停止,除非它们是苹果提供的后台模式之一。Biniou 发现他能够通过在视图控制器而不是应用程序委托中初始化来解决他的核心蓝牙背景问题。这对我来说没有意义。

我的应用程序确实在其 info.plist 中选择了核心蓝牙背景模式,因此它应该是这些背景模式之一。我发现当我的应用程序进入后台时,应用程序确实会继续处理数据。我看到来自每 100 毫秒运行一次的轮询循环的日志消息。

如果我从这些轮询循环中触发蓝牙 LE 写入,我就可以继续发送数据。问题是我无法确定发送数据的安全速率,要么速度很慢,要么数据有时会丢失。

我不确定如何最好地处理这个问题。任何建议,将不胜感激。似乎无论我在后台做什么,我都无法确定发送数据是否安全。

我看到这条评论表明唯一的解决方案是改变蓝牙设备连接到手机的方式,是这种情况吗?我不确定在这一点上改变硬件对我来说是一个选择。

理想的解决方案是找到一种方法将 CBCentralManager 放在其自己的串行队列中,但以这样一种方式创建该队列,即当应用程序进入后台时队列不会停止。如果有人知道该怎么做,我相信它会解决我的问题。

我当前的代码是这样的。当在 applicationDidFinishLaunchingWithOptions 回调中创建蓝牙服务到我的 AppDelegate

    self.cm = [[CBCentralManager alloc] initWithDelegate:self
                                                   queue:nil
                                                 options:@{CBCentralManagerOptionShowPowerAlertKey:@(0)}];
Run Code Online (Sandbox Code Playgroud)

或者使用应该工作但不工作的串行队列

    dispatch_queue_t bt_queue = dispatch_queue_create("BT_queue", 0);
    self.cm = [[CBCentralManager alloc] initWithDelegate:self
                                                   queue:bt_queue
                                                 options:@{CBCentralManagerOptionShowPowerAlertKey:@(0)}];
Run Code Online (Sandbox Code Playgroud)

当需要发送一些数据时,我使用其中之一

        [self.cbPeripheral writeValue:data
                    forCharacteristic:self.rxCharacteristic
                                 type:CBCharacteristicWriteWithoutResponse];
Run Code Online (Sandbox Code Playgroud)

如果我只是不停地调用这个 writeValue 而没有尝试检查发送数据是否安全。它最终会失败。

使用此代码建立连接后,将请求额外的后台执行时间

    - (void)   centralManager:(CBCentralManager *)central
     didConnectPeripheral:(CBPeripheral *)peripheral
    {
        UIApplication *app = [UIApplication sharedApplication];
        if (self.globalBackgroundTask != UIBackgroundTaskInvalid) {
            [app endBackgroundTask:self.globalBackgroundTask];
            self.globalBackgroundTask = UIBackgroundTaskInvalid;
        }
        self.globalBackgroundTask = [app beginBackgroundTaskWithExpirationHandler:^{
            [app endBackgroundTask:_globalBackgroundTask];
            _globalBackgroundTask = UIBackgroundTaskInvalid;
        }];
Run Code Online (Sandbox Code Playgroud)

关于如何给 CBCentralManager 一个不在 UI 线程上并且在应用程序进入后台时不会被关闭的队列的任何想法或建议,将不胜感激。如果那不可能,我需要尝试选择一种解决方法。