You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

iOS App终止状态下BLE传感器持续扫描与连接的技术实现咨询

实现iOS终止状态下持续监听BLE传感器的方案

针对你提出的两个需求(终止状态扫描BLE并识别keyEvent、类似Tile/Chipolo的后台监听连接),结合iOS蓝牙框架的特性,我整理了可落地的实现方案:

核心前提:理解iOS后台蓝牙的限制

首先得明确:iOS App处于终止状态时,系统不会允许持续主动扫描BLE设备,但可以利用系统的蓝牙后台唤醒机制——当你的App之前配对过的BLE设备发送特定广播时,系统会唤醒你的App,让你有短暂时间处理连接、识别事件。这也是Tile这类应用的核心原理。

具体实现步骤

1. 配置必要的Capabilities

  • 确保已开启Background Modes中的Bluetooth Central权限(你已经完成这一步,没问题)。
  • 同时在Info.plist中添加NSBluetoothAlwaysUsageDescription(iOS 13+也可搭配NSBluetoothPeripheralUsageDescription),清晰说明蓝牙使用目的,否则权限申请会直接失败。

2. 利用CBCentralManager的后台唤醒机制(针对终止状态)

当App终止后,只有满足以下条件,系统才会唤醒你的App:

  • 你的App之前已与目标BLE传感器建立过配对(或至少发现过并保存了它的identifier)。
  • BLE传感器发送的广播包中包含你App注册的服务UUID

具体代码实现要点:

  • 在App启动时(包括被系统唤醒的冷启动),初始化CBCentralManager并设置代理。
  • centralManagerDidUpdateState(_:)中,当蓝牙状态为.poweredOn时,调用retrievePeripherals(withIdentifiers:)获取之前配对过的传感器,尝试重新连接。
  • 实现centralManager(_:didDiscover:advertisementData:rssi:)方法:即使App在后台/终止状态,当发现带有目标UUID的广播时,系统会唤醒App,你可以在这里处理连接逻辑。
// 初始化CBCentralManager
lazy var centralManager: CBCentralManager = {
    let manager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
    return manager
}()

// App启动时的处理(包括冷启动被唤醒)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 初始化centralManager,系统会自动触发centralManagerDidUpdateState
    _ = self.centralManager
    return true
}

// 实现CBCentralManagerDelegate
extension AppDelegate: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        guard central.state == .poweredOn else {
            // 处理蓝牙未开启的情况
            return
        }
        // 尝试重新连接之前配对过的设备
        let savedPeripheralIDs = UserDefaults.standard.stringArray(forKey: "SavedPeripheralIDs")?.compactMap(UUID.init) ?? []
        let peripherals = central.retrievePeripherals(withIdentifiers: savedPeripheralIDs)
        peripherals.forEach { peripheral in
            central.connect(peripheral, options: [CBConnectPeripheralOptionNotifyOnDisconnectionKey: true])
        }
        // 同时开始扫描带有目标服务UUID的设备
        let targetServiceUUIDs = [CBUUID(string: "你的传感器服务UUID")]
        central.scanForPeripherals(withServices: targetServiceUUIDs, options: [
            CBCentralManagerScanOptionAllowDuplicatesKey: false
        ])
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        // 保存设备identifier,避免依赖易变的设备名称
        var savedIDs = UserDefaults.standard.stringArray(forKey: "SavedPeripheralIDs") ?? []
        if !savedIDs.contains(peripheral.uuid.uuidString) {
            savedIDs.append(peripheral.uuid.uuidString)
            UserDefaults.standard.set(savedIDs, forKey: "SavedPeripheralIDs")
        }
        // 尝试连接设备
        central.connect(peripheral, options: [CBConnectPeripheralOptionNotifyOnDisconnectionKey: true])
        // 停止扫描以节省电量
        central.stopScan()
    }

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        // 连接成功后,发现服务和特征,监听keyEvent相关的特征
        peripheral.delegate = self
        peripheral.discoverServices([CBUUID(string: "你的传感器服务UUID")])
    }
}

// 实现CBPeripheralDelegate监听keyEvent
extension AppDelegate: CBPeripheralDelegate {
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }
        for service in services {
            peripheral.discoverCharacteristics([CBUUID(string: "你的keyEvent特征UUID")], for: service)
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics where characteristic.uuid == CBUUID(string: "你的keyEvent特征UUID") {
            // 订阅特征通知,传感器发送keyEvent时会收到回调
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let data = characteristic.value else { return }
        // 解析数据,识别keyEvent
        handleKeyEvent(data: data)
    }
}

3. 优化后台唤醒与连接的可靠性

  • 确保BLE传感器的广播包中包含你的App注册的服务UUID,这是系统唤醒App的核心触发条件。如果传感器支持自定义广播,务必加入这个UUID。
  • 连接时使用CBConnectPeripheralOptionNotifyOnDisconnectionKey: true选项:当设备断开连接时,系统会再次唤醒App,你可以尝试重新连接。
  • 保存设备的identifierUserDefaultsKeychain,不要依赖设备名称(名称可被修改),identifier是iOS为每个BLE设备分配的唯一ID,设备重启也不会改变。

4. 处理keyEvent的注意事项

当App被系统唤醒处理keyEvent时,你只有10秒左右的时间窗口完成操作。如果需要执行耗时任务(比如发送通知),可以调用UIApplication.shared.beginBackgroundTask(expirationHandler:)申请额外后台时间:

func handleKeyEvent(data: Data) {
    var backgroundTask: UIBackgroundTaskIdentifier = .invalid
    backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
        UIApplication.shared.endBackgroundTask(backgroundTask)
        backgroundTask = .invalid
    }
    
    // 解析keyEvent类型
    let keyEventType = parseKeyEvent(data: data)
    // 发送本地通知提醒用户
    let content = UNMutableNotificationContent()
    content.title = "传感器触发"
    content.body = "检测到\(keyEventType)事件"
    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
    UNUserNotificationCenter.current().add(request)
    
    // 完成后结束后台任务
    if backgroundTask != .invalid {
        UIApplication.shared.endBackgroundTask(backgroundTask)
        backgroundTask = .invalid
    }
}

// 示例:解析keyEvent的方法
func parseKeyEvent(data: Data) -> String {
    // 根据传感器的数据格式解析,比如第一个字节代表按键类型
    guard let firstByte = data.first else { return "未知事件" }
    switch firstByte {
    case 0x01: return "按键按下"
    case 0x02: return "按键长按"
    default: return "未知事件"
    }
}

常见坑点

  • 不要试图在终止状态下持续扫描BLE,iOS系统不允许这种行为,强制扫描会被系统杀死进程。
  • 确保蓝牙权限申请正确:iOS 13+必须动态申请NSBluetoothAlwaysUsageDescription,否则后台蓝牙功能会完全失效。
  • 测试终止状态下的功能时,不要用Xcode调试(调试时App不会真正终止),应该点击Xcode的Stop按钮停止App,再用物理设备测试传感器触发唤醒。

内容的提问来源于stack exchange,提问作者Jitendra

火山引擎 最新活动