Bluetooth常见问题

590 阅读14分钟

一、基础概念

1. 蓝牙有几种技术标准?

  • 经典蓝牙 (Bluetooth Classic): 传输速率高、功耗大,主要用于音频传输、文件传输等场景。iOS 对经典蓝牙的设备间通信支持有限(主要通过 MFI 认证)。
  • 低功耗蓝牙 (Bluetooth Low Energy, BLE): 功耗低、数据量小,主要用于设备状态同步、传感器数据采集等。iOS 蓝牙开发主要围绕 BLE

2. iOS 中处理蓝牙的核心框架是哪个?

  • CoreBluetooth.framework
  • 它提供了与 BLE 设备交互的所有必要类和方法。

3. 解释 BLE 中的中心设备 (Central) 和外设 (Peripheral) 角色

  • 外设 (Peripheral): 广播数据的设备,例如智能手环、心率监测器。它持有数据。
  • 中心设备 (Central): 扫描并连接外设的设备,例如 iPhone。它消费数据。
  • 在 iOS 开发中,我们的 App 通常作为 Central 去连接其他 BLE 设备。 但 iOS 设备也可以作为 Peripheral(通过 CBPeripheralManager)。

二、核心流程与 API

4. 描述一个完整的 Central 端连接和数据交互流程

  1. 创建中心管理器

    centralManager = CBCentralManager(delegate: self, queue: nil)
    
  2. 监听蓝牙状态 (CBCentralManagerDelegate)

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            // 蓝牙已开启,开始扫描
            central.scanForPeripherals(withServices: nil, options: nil)
        }
    }
    
  3. 发现外设

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        // 检查外设名称或广播数据,找到目标设备
        if peripheral.name == "MyDevice" {
            self.targetPeripheral = peripheral
            central.stopScan()
            central.connect(peripheral, options: nil)
        }
    }
    
  4. 连接外设

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        // 连接成功,设置 Peripheral 的代理并开始发现服务
        peripheral.delegate = self
        peripheral.discoverServices(nil) // 传入 nil 发现所有服务
    }
    
  5. 发现服务 (CBPeripheralDelegate)

    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let services = peripheral.services {
            for service in services {
                // 根据目标服务的 UUID 进行过滤
                if service.uuid == CBUUID(string: "180D") { // 心率服务
                    peripheral.discoverCharacteristics(nil, for: service)
                }
            }
        }
    }
    
  6. 发现特征

    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    if let characteristics = service.characteristics {
        for characteristic in characteristics {
            let properties = characteristic.properties
            
            // 1. 通知类
            if properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: characteristic)
                print("启用通知: \(characteristic.uuid)")
            }
            
            if properties.contains(.indicate) {
                peripheral.setNotifyValue(true, for: characteristic)
                print("启用指示: \(characteristic.uuid)")
            }
            
            // 2. 读取类
            if properties.contains(.read) {
                peripheral.readValue(for: characteristic)
                print("读取特征: \(characteristic.uuid)")
            }
            
            // 3. 写入类
            if properties.contains(.write) {
                // 需要响应的写入
                print("支持写入(带响应): \(characteristic.uuid)")
            }
            
            if properties.contains(.writeWithoutResponse) {
                // 无响应的写入,速度更快但不可靠
                print("支持无响应写入: \(characteristic.uuid)")
            }
            
            // 4. 其他属性
            if properties.contains(.broadcast) {
                print("支持广播: \(characteristic.uuid)")
            }
            
            if properties.contains(.extendedProperties) {
                print("支持扩展属性: \(characteristic.uuid)")
            }
            
            if properties.contains(.authenticatedSignedWrites) {
                print("支持认证签名写入: \(characteristic.uuid)")
            }
            
            if properties.contains(.notifyEncryptionRequired) {
                print("需要加密的通知: \(characteristic.uuid)")
            }
            
            if properties.contains(.indicateEncryptionRequired) {
                print("需要加密的指示: \(characteristic.uuid)")
            }
        }
    }
    
    
  7. 数据交互

    • 读取数据peripheral.readValue(for: characteristic) 然后在 didUpdateValueFor 回调中获取数据。
    • 订阅通知peripheral.setNotifyValue(true, for: characteristic) 数据更新时,会在 didUpdateValueFor 回调中收到。
    • 写入数据peripheral.writeValue(data, for: characteristic, type: .withResponse) (或 .withoutResponse)
  8. 断开连接

    centralManager.cancelPeripheralConnection(peripheral)
    

5. 解释 BLE 的 GATT 结构

  • GATT (Generic Attribute Profile): 定义了 BLE 设备间数据传输的格式和结构。
  • 层级关系Device -> Service -> Characteristic -> Descriptor
    • Service (服务): 一个设备提供的特定功能,例如电池服务、设备信息服务。用 UUID 标识。
    • Characteristic (特征): 服务下的具体数据点,是数据交互的真正载体。例如电池电量、心率测量值。它有自己的 UUID、值和属性(读、写、通知等)。
    • Descriptor (描述符): 对 Characteristic 的额外描述,例如用户描述、特征格式等。

6. CBCharacteristicproperties 有哪些?各自含义是什么?

  • .read: 可读
  • .write: 可写(需要响应)
  • .writeWithoutResponse: 可写(不需要响应,速度快但不可靠)
  • .notify: 可订阅(外设主动推送数据,无确认)
  • .indicate: 可指示(外设主动推送数据,有确认,更可靠)
  • 等等。在操作一个特征前,必须检查其属性。

三、进阶问题

7. 后台模式下的蓝牙处理

  • 配置: 在 Capabilities 中开启 Background Modes 并勾选 Uses Bluetooth LE accessories
  • 限制
    • 扫描时需要使用 Service UUIDs(scanForPeripherals(withServices: [CBUUID], options: ...)),否则在后台扫描不到设备。
    • App 在后台或被挂起时,所有蓝牙事件都会在后台队列中被缓存,当 App 回到前台时会一并交付。
  • 状态保存与恢复: 通过实现 restoreIdentifiercentralManager(_:willRestoreState:) 方法,可以让系统在 App 被杀死后重新启动时恢复蓝牙连接和状态。

8. 如何保证蓝牙连接的稳定性和重连机制?

  • 监听断开事件: 实现 centralManager(_:didDisconnectPeripheral:error:) 代理方法。
  • 实现重连逻辑: 在断开回调中,根据错误码和业务逻辑进行重试连接。
  • 连接超时处理CoreBluetooth 没有原生超时,需要自己用 DispatchWorkItemTimer 实现。
  • 错误处理: 妥善处理常见的错误,如连接失败、服务发现失败等。

除了上述方法,还有更多提升蓝牙连接成功率的方案、失败原因分析和压测方法:

1. 连接参数优化

// 使用带选项的连接
let options: [String: Any] = [
    CBConnectPeripheralOptionNotifyOnConnectionKey: true,
    CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
    CBConnectPeripheralOptionNotifyOnNotificationKey: true,
    CBConnectPeripheralOptionEnableTransportBridgingKey: true,
    CBConnectPeripheralOptionRequiresANCS: false
]
centralManager.connect(peripheral, options: options)

这些是 Core Bluetooth 中连接外设时的选项参数,用于控制连接行为和系统通知。

各选项参数详解

1. CBConnectPeripheralOptionNotifyOnConnectionKey

CBConnectPeripheralOptionNotifyOnConnectionKey: true

作用:当外设连接成功时,即使应用在后台或挂起状态,系统也会显示通知提醒用户。

使用场景

let options: [String: Any] = [
    CBConnectPeripheralOptionNotifyOnConnectionKey: true
]
centralManager.connect(peripheral, options: options)

// 当连接成功时,即使用户没有打开你的App
// 系统会在通知中心显示:"[App名称] 已连接到 [设备名称]"

2. CBConnectPeripheralOptionNotifyOnDisconnectionKey

CBConnectPeripheralOptionNotifyOnDisconnectionKey: true

作用:当外设断开连接时(无论是主动断开还是意外断开),系统会显示通知。

使用场景

let options: [String: Any] = [
    CBConnectPeripheralOptionNotifyOnDisconnectionKey: true
]

// 当设备断开时,系统显示:
// "[App名称] 与 [设备名称] 的连接已断开"
// 这对于需要用户知道连接状态的医疗设备、安全设备等很重要

3. CBConnectPeripheralOptionNotifyOnNotificationKey

CBConnectPeripheralOptionNotifyOnNotificationKey: true

作用:当外设发送通知(特征值变化)而应用在后台时,系统会唤醒应用并允许处理数据。

使用场景

let options: [String: Any] = [
    CBConnectPeripheralOptionNotifyOnNotificationKey: true
]

// 应用在后台时,如果设备发送了特征值更新
// 系统会短暂唤醒应用(有几秒时间处理数据)
// 适合需要实时响应设备数据的场景

4. CBConnectPeripheralOptionEnableTransportBridgingKey

CBConnectPeripheralOptionEnableTransportBridgingKey: true

作用:在支持多设备协同的苹果生态中(如 iPhone + Apple Watch),允许连接在不同设备间桥接。

使用场景

let options: [String: Any] = [
    CBConnectPeripheralOptionEnableTransportBridgingKey: true
]

// 当用户有多个苹果设备时,连接信息可以在设备间共享
// 例如:在iPhone上配对的设备,在Apple Watch上也能识别
// 需要设备支持蓝牙4.0以上

5. CBConnectPeripheralOptionRequiresANCS

CBConnectPeripheralOptionRequiresANCS: false

作用:指定连接是否要求 ANCS(Apple Notification Center Service)。

使用场景

let options: [String: Any] = [
    CBConnectPeripheralOptionRequiresANCS: false
]

// ANCS 是苹果通知中心服务,允许蓝牙设备显示iPhone通知
// 设为 false 表示不强制要求设备支持ANCS
// 通常用于非通知类设备(传感器、控制器等)

实际应用示例

医疗设备连接配置

func connectMedicalDevice(_ peripheral: CBPeripheral) {
    let options: [String: Any] = [
        CBConnectPeripheralOptionNotifyOnConnectionKey: true,
        CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
        CBConnectPeripheralOptionNotifyOnNotificationKey: true,
        CBConnectPeripheralOptionEnableTransportBridgingKey: false, // 医疗设备通常不需要跨设备
        CBConnectPeripheralOptionRequiresANCS: false
    ]
    
    centralManager.connect(peripheral, options: options)
}

智能家居设备配置

func connectHomeDevice(_ peripheral: CBPeripheral) {
    let options: [String: Any] = [
        CBConnectPeripheralOptionNotifyOnConnectionKey: false, // 家居设备连接不需要通知
        CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, // 断开需要知道
        CBConnectPeripheralOptionNotifyOnNotificationKey: true, // 需要接收状态更新
        CBConnectPeripheralOptionEnableTransportBridgingKey: true, // 支持多设备控制
        CBConnectPeripheralOptionRequiresANCS: false
    ]
    
    centralManager.connect(peripheral, options: options)
}

音频设备配置

func connectAudioDevice(_ peripheral: CBPeripheral) {
    let options: [String: Any] = [
        CBConnectPeripheralOptionNotifyOnConnectionKey: true, // 连接成功提示
        CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, // 断开提示
        CBConnectPeripheralOptionNotifyOnNotificationKey: false,
        CBConnectPeripheralOptionEnableTransportBridgingKey: true, // 支持跨设备音频
        CBConnectPeripheralOptionRequiresANCS: true // 音频设备通常需要显示通知
    ]
    
    centralManager.connect(peripheral, options: options)
}

注意事项

  1. 后台模式:使用这些选项通常需要在 Capabilities 中开启 Bluetooth LE 后台模式
  2. 用户隐私:系统通知会显示App名称,要考虑用户体验
  3. 电量影响NotifyOnNotificationKey 会频繁唤醒App,影响电量
  4. 审核要求:如果滥用这些选项可能导致App Store审核被拒

这些选项主要用于提升用户体验和设备连接的可靠性,根据具体业务场景选择合适的配置。

2. 扫描策略优化

func startOptimizedScan() {
    let scanOptions: [String: Any] = [
        CBCentralManagerScanOptionAllowDuplicatesKey: false,
        CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [yourServiceUUID]
    ]
    centralManager.scanForPeripherals(withServices: [yourServiceUUID], options: scanOptions)
    
    // 扫描超时控制
    DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
        self.centralManager.stopScan()
    }
}

旨在提高扫描效率并减少资源消耗。

方法整体作用

这是一个优化的蓝牙设备扫描函数,通过限制扫描条件和添加超时控制,实现精准、高效的设备发现

1. 扫描选项配置

let scanOptions: [String: Any] = [
    CBCentralManagerScanOptionAllowDuplicatesKey: false,
    CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [yourServiceUUID]
]

CBCentralManagerScanOptionAllowDuplicatesKey: false

作用:禁止重复报告同一设备

  • true:每次收到设备广播都回调,会频繁收到同一设备
  • false:每个设备只回调一次(除非丢失后重新发现)
  • 优势:减少不必要的回调,节省电量和CPU资源

CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [yourServiceUUID]

作用:只扫描包含指定服务的设备

  • 只发现广播中包含 yourServiceUUID 服务的设备
  • 过滤掉不相关的蓝牙设备
  • 优势:精准扫描,减少干扰设备

2. 开始扫描

centralManager.scanForPeripherals(withServices: [yourServiceUUID], options: scanOptions)

作用:启动蓝牙扫描,双重过滤机制

  • withServices: [yourServiceUUID]:系统层过滤,只发现目标服务设备
  • scanOptions:应用层进一步控制扫描行为

3. 超时控制

DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
    self.centralManager.stopScan()
}

作用:10秒后自动停止扫描

  • 防止无限扫描消耗电量
  • 避免用户等待时间过长
  • 10秒是经验值,可根据业务调整

优化前后的对比

优化前(基础扫描)

// 问题:会发现所有蓝牙设备,耗电且效率低
func startBasicScan() {
    centralManager.scanForPeripherals(withServices: nil, options: nil)
    // 没有超时控制,需要手动调用 stopScan
}

优化后(精准扫描)

// 优势:只找目标设备,自动停止,高效省电
func startOptimizedScan() {
    let scanOptions: [String: Any] = [
        CBCentralManagerScanOptionAllowDuplicatesKey: false,      // 去重
        CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [yourServiceUUID] // 过滤
    ]
    centralManager.scanForPeripherals(withServices: [yourServiceUUID], options: scanOptions)
    
    // 自动超时停止
    DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
        self.centralManager.stopScan()
    }
}

实际应用场景

场景1:智能家居设备配网

class SmartHomeManager {
    let iotServiceUUID = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
    
    func scanForSmartDevices() {
        let scanOptions: [String: Any] = [
            CBCentralManagerScanOptionAllowDuplicatesKey: false,
            CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [iotServiceUUID]
        ]
        
        centralManager.scanForPeripherals(withServices: [iotServiceUUID], options: scanOptions)
        
        // 智能家居设备通常快速响应,8秒足够
        DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
            self.centralManager.stopScan()
            self.handleScanTimeout()
        }
    }
}

场景2:医疗设备扫描

class MedicalDeviceManager {
    let healthServiceUUID = CBUUID(string: "180A") // 设备信息服务
    
    func scanForMedicalDevices() {
        let scanOptions: [String: Any] = [
            CBCentralManagerScanOptionAllowDuplicatesKey: false, // 医疗设备不需要重复数据
            CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [healthServiceUUID]
        ]
        
        centralManager.scanForPeripherals(withServices: [healthServiceUUID], options: scanOptions)
        
        // 医疗设备可能需要更长时间
        DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
            self.centralManager.stopScan()
        }
    }
}

场景3:多服务设备扫描

class MultiServiceScanner {
    let primaryServiceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789012")
    let secondaryServiceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789013")
    
    func scanForMultiServiceDevices() {
        let targetServices = [primaryServiceUUID, secondaryServiceUUID]
        
        let scanOptions: [String: Any] = [
            CBCentralManagerScanOptionAllowDuplicatesKey: false,
            CBCentralManagerScanOptionSolicitedServiceUUIDsKey: targetServices
        ]
        
        centralManager.scanForPeripherals(withServices: targetServices, options: scanOptions)
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 12) {
            self.centralManager.stopScan()
        }
    }
}
  1. 省电:不扫描无关设备,自动停止
  2. 高效:快速找到目标设备,减少用户等待
  3. 精准:双重过滤机制,只发现相关设备
  4. 稳定:避免重复回调,减少内存占用

注意事项

func startOptimizedScan() {
    // 先停止之前的扫描,避免多重扫描
    centralManager.stopScan()
    
    let scanOptions: [String: Any] = [
        CBCentralManagerScanOptionAllowDuplicatesKey: false,
        CBCentralManagerScanOptionSolicitedServiceUUIDsKey: [yourServiceUUID]
    ]
    
    centralManager.scanForPeripherals(withServices: [yourServiceUUID], options: scanOptions)
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
        self.centralManager.stopScan()
        // 可以在这里添加扫描完成后的逻辑
        self.onScanComplete()
    }
}

3. 连接状态管理

class BluetoothManager: NSObject {
    private var connectionTimer: DispatchWorkItem?
    private var retryCount = 0
    private let maxRetryCount = 3
    private var connectionStartTime: Date?
    
    func connectWithStrategy(_ peripheral: CBPeripheral) {
        guard retryCount < maxRetryCount else {
            print("超过最大重试次数")
            return
        }
        
        connectionStartTime = Date()
        setupConnectionTimeout()
        
        // 检查系统蓝牙状态
        guard centralManager.state == .poweredOn else {
            handleBluetoothStateIssue()
            return
        }
        
        // 避免重复连接
        if peripheral.state != .connected && peripheral.state != .connecting {
            centralManager.connect(peripheral, options: options)
        }
    }
    
    private func setupConnectionTimeout() {
        connectionTimer?.cancel()
        connectionTimer = DispatchWorkItem { [weak self] in
            self?.handleConnectionTimeout()
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: connectionTimer!)
    }
}

4. 设备缓存策略

// 使用 identifier 重连已知设备
func reconnectToKnownPeripherals() {
    let knownIdentifiers = retrieveKnownPeripheralIdentifiers()
    let peripherals = centralManager.retrievePeripherals(withIdentifiers: knownIdentifiers)
    
    for peripheral in peripherals {
        if peripheral.state != .connected {
            connectWithStrategy(peripheral)
        }
    }
}

这个方法通过系统缓存的重连机制,**自动重新连接用户之前成功连接过的蓝牙设备**,提升用户体验

### 1. 获取已知设备标识
```swift
let knownIdentifiers = retrieveKnownPeripheralIdentifiers()

作用:从本地存储(如 UserDefaults、数据库)获取之前成功连接过的设备标识符数组。

可能的 retrieveKnownPeripheralIdentifiers 实现

private func retrieveKnownPeripheralIdentifiers() -> [UUID] {
    guard let savedIdentifiers = UserDefaults.standard.array(forKey: "KnownPeripheralIdentifiers") as? [String] else {
        return []
    }
    return savedIdentifiers.compactMap { UUID(uuidString: $0) }
}

2. 从系统缓存检索外设

let peripherals = centralManager.retrievePeripherals(withIdentifiers: knownIdentifiers)

作用:Core Bluetooth 系统会尝试从缓存中找回这些标识符对应的外设对象。

重要特性

  • 只返回当前在系统蓝牙范围内的设备
  • 设备必须曾经成功连接并配对过
  • 系统维护一个已知设备的缓存列表

3. 遍历并连接设备

for peripheral in peripherals {
    if peripheral.state != .connected {
        connectWithStrategy(peripheral)
    }
}

作用:检查每个设备状态,对未连接的设备启动重连流程。

状态检查的意义

  • .connected:已连接,跳过
  • .disconnected:未连接,启动重连
  • .connecting:连接中,通常跳过避免重复
  • .disconnecting:断开中,等待完成后重连

完整的重连系统实现

1. 设备标识管理

class BluetoothConnectionManager {
    private let knownDevicesKey = "KnownPeripheralIdentifiers"
    
    // 保存成功连接的设备
    func saveConnectedPeripheral(_ peripheral: CBPeripheral) {
        var knownIdentifiers = retrieveKnownPeripheralIdentifiers()
        let identifierString = peripheral.identifier.uuidString
        
        if !knownIdentifiers.contains(peripheral.identifier) {
            knownIdentifiers.append(peripheral.identifier)
            saveIdentifiersToStorage(knownIdentifiers)
        }
    }
    
    // 移除设备
    func removeKnownPeripheral(_ peripheral: CBPeripheral) {
        var knownIdentifiers = retrieveKnownPeripheralIdentifiers()
        knownIdentifiers.removeAll { $0 == peripheral.identifier }
        saveIdentifiersToStorage(knownIdentifiers)
    }
    
    private func saveIdentifiersToStorage(_ identifiers: [UUID]) {
        let identifierStrings = identifiers.map { $0.uuidString }
        UserDefaults.standard.set(identifierStrings, forKey: knownDevicesKey)
    }
}

2. 增强的重连策略

func reconnectToKnownPeripherals() {
    let knownIdentifiers = retrieveKnownPeripheralIdentifiers()
    
    guard !knownIdentifiers.isEmpty else {
        print("没有已知设备需要重连")
        return
    }
    
    let peripherals = centralManager.retrievePeripherals(withIdentifiers: knownIdentifiers)
    
    print("找到 \(peripherals.count)/\(knownIdentifiers.count) 个已知设备在范围内")
    
    for peripheral in peripherals {
        switch peripheral.state {
        case .disconnected:
            print("重连设备: \(peripheral.name ?? "未知设备")")
            connectWithStrategy(peripheral)
            
        case .connected:
            print("设备已连接: \(peripheral.name ?? "未知设备")")
            // 可以在这里触发服务发现等后续操作
            discoverServicesForConnectedPeripheral(peripheral)
            
        case .connecting:
            print("设备连接中: \(peripheral.name ?? "未知设备")")
            
        case .disconnecting:
            print("设备断开中: \(peripheral.name ?? "未知设备")")
            
        @unknown default:
            break
        }
    }
}

3. 智能连接策略

func connectWithStrategy(_ peripheral: CBPeripheral) {
    let connectionOptions: [String: Any] = [
        CBConnectPeripheralOptionNotifyOnConnectionKey: true,
        CBConnectPeripheralOptionNotifyOnDisconnectionKey: true
    ]
    
    // 设置连接超时
    setupConnectionTimeout(for: peripheral)
    
    // 记录连接尝试
    logConnectionAttempt(for: peripheral)
    
    centralManager.connect(peripheral, options: connectionOptions)
}

private func setupConnectionTimeout(for peripheral: CBPeripheral) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
        if peripheral.state == .connecting {
            print("连接超时,取消连接: \(peripheral.name ?? "未知设备")")
            self?.centralManager.cancelPeripheralConnection(peripheral)
        }
    }
}

实际应用场景

场景1:App启动时自动重连

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // App启动时尝试重连已知设备
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.bluetoothManager.reconnectToKnownPeripherals()
        }
        
        return true
    }
}

场景2:从后台返回时重连

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
    }
    
    @objc func appDidBecomeActive() {
        // 从后台返回时检查重连
        bluetoothManager.reconnectToKnownPeripherals()
    }
}

场景3:网络状态变化时重连

class BluetoothManager {
    private func setupNetworkObserver() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(networkStatusChanged),
            name: .networkStatusChanged, // 自定义通知
            object: nil
        )
    }
    
    @objc private func networkStatusChanged() {
        // 网络恢复时尝试重连设备
        reconnectToKnownPeripherals()
    }
}

优势与价值

  1. 用户体验:用户无需手动重新连接设备
  2. 自动化:App能智能恢复之前的连接状态
  3. 效率:只连接在范围内的已知设备
  4. 稳定性:系统级缓存提供可靠的重连机制

注意事项

func reconnectToKnownPeripherals() {
    // 1. 检查蓝牙状态
    guard centralManager.state == .poweredOn else {
        print("蓝牙未开启,无法重连")
        return
    }
    
    // 2. 限制重连频率,避免过于频繁
    if Date().timeIntervalSince(lastReconnectAttempt) < 30 {
        return
    }
    lastReconnectAttempt = Date()
    
    let knownIdentifiers = retrieveKnownPeripheralIdentifiers()
    let peripherals = centralManager.retrievePeripherals(withIdentifiers: knownIdentifiers)
    
    for peripheral in peripherals {
        if peripheral.state != .connected {
            connectWithStrategy(peripheral)
        }
    }
    
    // 3. 记录重连日志
    logReconnectionAttempt(peripherals.count)
}

5. 信号强度筛选

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, 
                  advertisementData: [String : Any], rssi RSSI: NSNumber) {
    // 只连接信号强度足够的设备
    if RSSI.intValue > -75 {  // 可根据实际情况调整阈值
        connectablePeripherals[peripheral.identifier] = peripheral
    }
}

1. 系统层面原因

func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
    if let error = error as? CBError {
        switch error.code {
        case .unknown:
            print("未知错误,可能是系统蓝牙栈问题")
        case .connectionTimeout:
            print("连接超时,设备可能不在范围内")
        case .peerRemovedPairingInformation:
            print("配对信息被清除,需要重新配对")
        case .encryptionTimedOut:
            print("加密超时")
        case .tooManyLEPairedDevices:
            print("系统蓝牙配对设备数量超限")
        default:
            print("其他错误: \(error)")
        }
    }
}

2. 设备层面原因

  • 距离过远:RSSI 信号弱
  • 设备忙:设备正在处理其他连接
  • 电量不足:设备进入低功耗模式
  • 硬件故障:设备蓝牙模块异常

3. 应用层面原因

  • 资源竞争:多个线程同时操作蓝牙
  • 状态管理错误:未正确管理连接状态
  • 内存压力:应用内存不足导致连接被终止

压测方案

1. 自动化测试框架

class BluetoothStressTest {
    private var testResults: [TestResult] = []
    private var testCount = 0
    private let totalTests = 100
    
    func startStressTest() {
        for i in 0..<totalTests {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 2) {
                self.executeSingleTest(iteration: i)
            }
        }
    }
    
    private func executeSingleTest(iteration: Int) {
        let testStartTime = Date()
        let peripheral = discoverPeripheral() // 发现设备
        
        connectWithStrategy(peripheral) { [weak self] success, duration, error in
            let result = TestResult(
                iteration: iteration,
                success: success,
                duration: duration,
                error: error,
                timestamp: Date()
            )
            self?.testResults.append(result)
            
            // 立即断开,准备下一次测试
            if success {
                self?.centralManager.cancelPeripheralConnection(peripheral)
            }
        }
    }
}

2. 性能指标监控

struct TestResult {
    let iteration: Int
    let success: Bool
    let duration: TimeInterval
    let error: Error?
    let timestamp: Date
    let deviceModel: String
    let iOSVersion: String
    let rssi: Int
}

class PerformanceMonitor {
    func generateReport() {
        let totalTests = testResults.count
        let successCount = testResults.filter { $0.success }.count
        let successRate = Double(successCount) / Double(totalTests) * 100
        
        let durations = testResults.compactMap { $0.duration }
        let avgDuration = durations.reduce(0, +) / Double(durations.count)
        
        print("""
        压力测试报告:
        - 总测试次数: \(totalTests)
        - 成功率: \(String(format: "%.2f", successRate))%
        - 平均连接时间: \(String(format: "%.2f", avgDuration))秒
        - 失败分布: \(getErrorDistribution())
        """)
    }
}

3. 真实场景模拟

class RealWorldSimulator {
    func simulateRealUsage() {
        // 模拟不同距离
        simulateDistanceVariation()
        
        // 模拟信号干扰
        simulateSignalInterference()
        
        // 模拟多设备竞争
        simulateMultipleDevices()
        
        // 模拟应用状态变化
        simulateAppStateChanges()
    }
    
    private func simulateAppStateChanges() {
        // 前后台切换
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillResignActive),
            name: UIApplication.willResignActiveNotification,
            object: nil
        )
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidBecomeActive),
            name: UIApplication.didBecomeActiveNotification,
            object: nil
        )
    }
}

4. 环境变量测试

func testUnderDifferentConditions() {
    // 测试不同系统版本
    // 测试不同设备型号
    // 测试不同蓝牙版本
    // 测试不同距离和障碍物情况
}

最佳实践总结

  1. 分级重试:根据错误类型采用不同的重试策略
  2. 用户引导:在连接失败时给出明确的用户指引
  3. 降级方案:准备备用的配网方式(如 SoftAP)
  4. 数据上报:收集连接失败数据用于优化
  5. 设备指纹:记录设备特征,优化重连策略

这些方案可以显著提升蓝牙连接的成功率和稳定性,特别是在复杂的物联网环境中。

9. 蓝牙配对和绑定 (Bonding) 的区别?

  • 配对 (Pairing): 一个一次性的过程,用于交换密钥、认证身份,建立安全的连接。
  • 绑定 (Bonding): 在配对成功后,将交换的密钥(LTK, Long-Term Key)存储起来,以便后续重新连接时无需再次配对。在 iOS 上,这个过程由系统自动管理。

10. 作为 Peripheral 端 (CBPeripheralManager) 需要做什么?

  • 创建 CBPeripheralManager
  • 设置服务和特征(CBMutableServiceCBMutableCharacteristic)。
  • 发布服务到本地数据库。
  • 开始广播。
  • 处理 Central 的订阅、读请求和写请求。

四、实战与经验

11. 你在项目中遇到的蓝牙难点是什么?如何解决的?

这是一个开放性问题,考察实际经验。可能的答案:

  • 连接不稳定: 实现了指数退避算法的重连机制。
  • 数据分包与组包: BLE 单次传输数据量有限(通常是 20 字节),需要设计协议来处理长数据。
  • 多设备连接管理: 使用字典或数组管理多个 CBPeripheral 实例。
  • 不同厂商设备兼容性: 处理非标准的 UUID 或不符合规范的 GATT 结构。

12. 如何调试蓝牙问题?

  • 使用 LightBluenRF Connect 等第三方 App 模拟 Peripheral 或扫描设备,验证硬件本身是否正常。
  • 在 Xcode 中查看 CoreBluetooth 的日志(有时需要额外的系统日志工具)。
  • 检查所有的 Delegate 回调,特别是错误回调。
  • 使用 Packet Logger(需要苹果开发者账号)进行底层 HCI 日志抓取,这是最强大的调试手段。

13. 了解 Bluetooth 5.0 的新特性吗?

  • 2M PHY: 更高的传输速率。
  • LE Audio: 新一代蓝牙音频标准。
  • Long Range: 更长的传输距离。
  • Advertising Extensions: 更强大的广播数据能力。
  • 注意: 这些特性需要手机硬件和外围设备同时支持,并且 iOS API 可能对部分特性有版本要求。