等待 iOS Swift CBPeripheralDelegate 完成的正确方法?

Posted

技术标签:

【中文标题】等待 iOS Swift CBPeripheralDelegate 完成的正确方法?【英文标题】:Proper way to wait for iOS Swift CBPeripheralDelegate to complete? 【发布时间】:2020-09-07 21:21:20 【问题描述】:

在进行蓝牙通信时,通常会遇到这样一种情况,即进行调用并在委托中获得响应,例如如下所示的特征发现:

func discoverCharacteristics(device: CBPeripheral)

    servicesCount = device.services!.count

    for service in device.services!
    
        print("Discovering characteristics for service \(service.uuid)")
        device.discoverCharacteristics([], for: service)
    

现在这个发现不是针对特定设备,而是针对遵循蓝牙 SIG 服务/配置文件的健康设备,所以我不确切知道它们可能具有哪些服务,也不知道每个服务中可能有多少特征.该方法是异步的,答案在以下委托方法中发出信号:

// Discovered Characteristics event
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)

    for characteristic in service.characteristics!
    
        print("Found characteristic \(characteristic.uuid)")
    
    servicesCount = servicesCount - 1;
    print("Characteristics sets left: \(servicesCount)")
    if servicesCount == 0
    
        print ("Found all characteristics")
        DispatchQueue.main.async
            self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
        
        self.device = peripheral
        self.handleMds()
    

现在我需要等到发现完成后才能进行下一步,因为我接下来要做什么通常取决于我得到了什么。在 Java 和 android 中,我所做的是在调用方法中等待 CountDownLatch,然后我在回调中向闩锁发出信号以释放该等待。

CountDownLatch 的 ios 等效项似乎是 DispatchSemaphore。但是,这样做显然会阻塞系统,并且不会调用任何委托。所以我所做的(如上面的代码所示)是用服务的数量初始化一个变量servicesCount,并在每次发出信号时在委托回调中递减它。当它变为零时,我就完成了,然后我进行下一步。

这种方法行得通,但看起来很老套;它不可能是正确的。当我需要对 DIS 特性、特性、各种时间服务等进行多次读取时,它开始变得非常混乱。所以我想知道什么是等待代表的正确方法在继续前进之前获得信号?回想一下,我不知道这些设备可能具有哪些服务或特性。

【问题讨论】:

【参考方案1】:

首先,如果您已经为 Android 实现了 CountDownLatch,那么您可以为 iOS 执行相同的实现。是的,Swift 没有内置的CountDownLatch,但是来自优步的好人created a good implementation。

另一种选择是依赖一个变量,就像你做的那样,但是让它成为原子的。网上有多种实现,包括同一个 Uber 库中的一种。另一个例子是RxSwift library。还有许多其他变体。原子变量将为变量的读/写操作提供线程安全。

但可能最快捷的方法是拥有一个 DispatchGroup。看起来像这样:

let dispatchGroup = DispatchGroup() // instance-level definition

// ...

func discoverCharacteristics

    for service in device.services!
    
        dispatchGroup.enter() 
        // ...
    

    dispatchGroup.notify(queue: .main) 

        // All done
        print ("Found all characteristics")
        DispatchQueue.main.async
            self.btleManager!.btleManagerDelegate.statusEvent(device: peripheral, statusEvent: Btle.CHARACTERISTICS_DISCOVERED)
        
        self.device = peripheral
        self.handleMds()
    

// ...

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)

    // ...
    dispatchGroup.leave()

换句话说,您在即将提交请求时进入组,在处理请求时离开。当所有项目离开组时,notify 将使用您提供的块执行。

【讨论】:

这听起来像是一个很好的解决方案 - 如果它有效 - 因为我使用 DispatchSemaphore 做了类似的事情。拨打电话后,我进入了等待状态,在委托中,如果它被触发,我会释放信号量。但是,这样做会导致代表永远不会被调用。如果这种 DispatchGroup 方法有效,我会很乐意接受。我仍然不明白为什么 DispatchSemaphore 不起作用...我也喜欢这个想法,因为我不能使用任何第三方库。 @BrianReinhold 一个问题是wait 不应阻塞主线程(换句话说:wait 线程不应在主线程上调用)。对于调度组和信号量,它以相同的方式应用,但你注意到我使用notify 而不是等待,它不会阻塞任何线程,它只是在条件为真之前不会执行 工作就像一个魅力!我必须在主线程或 UI 线程上并且没有意识到它。我试图模拟一个位于主线程而不是 UI 线程上的 Android 服务。【参考方案2】:

每次拨打device.discoverCharacteristics 时,您只会收到一次拨打didDiscoverCharacteristicsFor 的电话。也就是说,didDiscoverCharacteristicsFor 调用仅在发现所有服务特征后才进行。

您已将peripheral 传递给委托调用,因此您拥有了解委托调用上下文所需的信息。没有必要“等待”。如果您同时发现多个外围设备/服务的数据,则可以为每个外围设备或服务使用一个简单的状态机。

如果您需要在发现完成后采取一些措施,您甚至可以保留一组未处于完全发现状态的外围设备;例如一旦外围设备的发现完成,您就可以从集合中移除外围设备。如果该集合为空,则所有设备的发现都已完成。

所有这些状态都属于您的模型。您的委托实现所需要做的就是使用已发现的数据更新模型。

【讨论】:

以上是关于等待 iOS Swift CBPeripheralDelegate 完成的正确方法?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ios swift 中连接蓝牙低功耗?

iOS CBPeripheral 连接问题

iOS 11 CoreBluetooth:无法删除关键路径“委托”的观察者 CBPeripheral

是否可以使用 CBPeripheral 通过蓝牙从 iOS 连接到 macOS

为啥 CBPeripheral 标识符和 UUID 不同?

在连接之前识别 CBPeripheral 的类型