首先的问题是使用核心蓝牙作为中心角色将数据发送到蓝牙 LE 设备的最佳方式是什么。需要处理数据,如果在 UI 线程上运行,这需要足够的时间来导致 UI 线程出现问题。用户将在手机应用程序打开的情况下启动该过程,然后继续使用应用程序或关闭应用程序并期望数据继续发送到设备。
我发现了 2 种非常糟糕的方法,它们似乎有效
- 将蓝牙 CBCentralManager 对象放在主队列上,有阻塞 UI 的风险
- 忽略来自 iOS 蓝牙堆栈的指示,即它尚未准备好传输并有丢失数据的风险。
这似乎源于 iOS 线程/调度队列以及 iOS 蓝牙内部。
蓝牙 LE iOS 应用程序连接到蓝牙 LE 设备作为中心角色。CBCentralManager 根据苹果文档进行初始化。队列定义为:
用于调度中心角色事件的调度队列。如果值为 nil,则中央管理器使用主队列分派中央角色事件。
正如vladiulianbogdan 对 Swift 的回答所建议的那样:为蓝牙中央管理器选择队列,我们应该为 CBCentralManager 创建一个串行队列。这似乎是有道理的,有一段时间我一直在遵循这个建议。还有对 Swift CoreBluetooth 的 allprog 评论:如果 CentralManager 在单独的线程中运行 表明主队列将被挂起,但其他队列不会,这与我所看到的相反。
在为蓝牙使用串行队列时,最好使用与主线程不同的队列。有一个问题:回调:
-(void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral *)peripheral
{
[self sendNextBluetoothLePacket];
}
别再被叫了。还有另一种方法可以检查外设是否准备好发送更多数据,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)}];
或者使用应该工作但不能工作的串行队列
dispatch_queue_t bt_queue = dispatch_queue_create("BT_queue", 0);
self.cm = [[CBCentralManager alloc] initWithDelegate:self
queue:bt_queue
options:@{CBCentralManagerOptionShowPowerAlertKey:@(0)}];
当需要发送一些数据时,我使用其中之一
[self.cbPeripheral writeValue:data
forCharacteristic:self.rxCharacteristic
type:CBCharacteristicWriteWithoutResponse];
如果我只是不停地调用这个 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;
}];
对此的任何想法或关于我如何给 CBCentralManager 一个不在 UI 线程上并且在应用程序进入后台时不会被关闭的队列的建议将不胜感激。如果那不可能,我需要尝试选择一种解决方法。