CoreBluetooth 连接智能设备

7 阅读7分钟

iOS CoreBluetooth 多链路管理

CoreBluetooth 是 iOS 中用于蓝牙低功耗(BLE)开发的核心框架,多链路管理指同时连接、管理多个 BLE 外设设备,是工业、智能家居等场景的常见需求。下面从核心思路、完整实现、关键优化三个维度讲解。

一、核心设计思路

多链路管理的核心挑战是:

  1. 避免并发连接导致的系统资源竞争
  2. 统一管理多个外设的连接状态、数据交互
  3. 处理断线重连、连接超时等异常场景

设计原则:

  • 使用队列管理连接请求,避免同时发起多个连接
  • 用数据模型封装每个外设的信息(UUID、状态、连接参数)
  • 监听系统蓝牙状态,统一处理蓝牙开关、权限等全局事件
  • 对每个外设的操作(连接、读写、断开)做独立的异常处理

二、完整实现代码

1. 先定义核心数据模型

封装外设信息,便于统一管理:

import CoreBluetooth

// 外设连接状态
enum BLEPeripheralState {
    case idle          // 空闲(未连接)
    case connecting    // 连接中
    case connected     // 已连接
    case disconnecting // 断开中
    case failed        // 连接失败
}

// 外设模型
class BLEPeripheralModel {
    let peripheral: CBPeripheral          // 外设对象
    let identifier: UUID                  // 外设唯一标识
    var state: BLEPeripheralState = .idle // 连接状态
    var connectTimeout: TimeInterval = 10 // 连接超时时间
    var retryCount: Int = 0               // 重连次数
    let maxRetryCount: Int = 3            // 最大重连次数
    
    // 数据交互相关
    var discoveredServices: [CBService]?
    var discoveredCharacteristics: [CBCharacteristic]?
    
    init(peripheral: CBPeripheral) {
        self.peripheral = peripheral
        self.identifier = peripheral.identifier
    }
}

2. 多链路管理核心类

实现外设扫描、连接、数据交互、状态管理的完整逻辑:

import CoreBluetooth
import UIKit

class BLECentralManager: NSObject {
    // MARK: - 核心属性
    private let centralManager: CBCentralManager
    private let bleQueue = DispatchQueue(label: "com.ble.central.queue", qos: .utility) // 蓝牙操作队列
    private let connectQueue = DispatchQueue(label: "com.ble.connect.queue") // 连接请求队列
    
    // 已发现/已管理的外设列表
    private var peripheralModels: [UUID: BLEPeripheralModel] = [:]
    // 连接超时定时器
    private var connectTimers: [UUID: Timer] = [:]
    
    // 单例(全局唯一的蓝牙管理器)
    static let shared = BLECentralManager()
    
    // 状态回调(供外部监听)
    var onPeripheralStateChanged: ((UUID, BLEPeripheralState) -> Void)?
    var onDataReceived: ((UUID, Data) -> Void)?
    
    // MARK: - 初始化
    private override init() {
        centralManager = CBCentralManager(delegate: nil, queue: bleQueue)
        super.init()
        centralManager.delegate = self
    }
    
    // MARK: - 对外API
    
    /// 开始扫描外设
    /// - Parameter serviceUUIDs: 要扫描的服务UUID(nil表示扫描所有)
    func startScanning(serviceUUIDs: [CBUUID]? = nil) {
        guard centralManager.state == .poweredOn else {
            print("蓝牙未开启,无法扫描")
            return
        }
        // 避免重复扫描
        if centralManager.isScanning { return }
        // 设置扫描参数:不重复发现、扫描20秒后停止
        centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [
            CBCentralManagerScanOptionAllowDuplicatesKey: false
        ])
        
        // 自动停止扫描(防止耗电)
        bleQueue.asyncAfter(deadline: .now() + 20) { [weak self] in
            self?.stopScanning()
        }
    }
    
    /// 停止扫描
    func stopScanning() {
        guard centralManager.isScanning else { return }
        centralManager.stopScan()
    }
    
    /// 连接指定外设
    /// - Parameter peripheralID: 外设唯一标识
    func connectPeripheral(peripheralID: UUID) {
        guard let model = peripheralModels[peripheralID],
              centralManager.state == .poweredOn else {
            print("外设不存在或蓝牙未开启")
            return
        }
        
        // 避免重复连接
        if model.state == .connecting || model.state == .connected { return }
        
        // 加入连接队列,串行执行
        connectQueue.async { [weak self] in
            guard let self = self else { return }
            
            // 更新状态
            model.state = .connecting
            self.onPeripheralStateChanged?(model.identifier, .connecting)
            
            // 发起连接
            self.centralManager.connect(model.peripheral, options: [
                CBCentralManagerConnectOptionNotifyOnConnectionKey: true,
                CBCentralManagerConnectOptionNotifyOnDisconnectionKey: true
            ])
            
            // 设置连接超时
            self.setupConnectTimeoutTimer(for: model.identifier)
        }
    }
    
    /// 断开指定外设连接
    /// - Parameter peripheralID: 外设唯一标识
    func disconnectPeripheral(peripheralID: UUID) {
        guard let model = peripheralModels[peripheralID],
              model.state == .connected else { return }
        
        model.state = .disconnecting
        onPeripheralStateChanged?(model.identifier, .disconnecting)
        
        centralManager.cancelPeripheralConnection(model.peripheral)
        // 清除超时定时器
        connectTimers[peripheralID]?.invalidate()
        connectTimers.removeValue(forKey: peripheralID)
    }
    
    /// 断开所有外设连接
    func disconnectAllPeripherals() {
        peripheralModels.forEach { disconnectPeripheral(peripheralID: $0.key) }
    }
    
    /// 读取外设特征值
    /// - Parameters:
    ///   - peripheralID: 外设ID
    ///   - characteristicUUID: 特征UUID
    func readCharacteristic(peripheralID: UUID, characteristicUUID: CBUUID) {
        guard let model = peripheralModels[peripheralID],
              model.state == .connected,
              let characteristics = model.discoveredCharacteristics else {
            print("外设未连接或特征未发现")
            return
        }
        
        guard let char = characteristics.first(where: { $0.uuid == characteristicUUID }) else {
            print("特征不存在")
            return
        }
        
        model.peripheral.readValue(for: char)
    }
    
    /// 向外设写入数据
    /// - Parameters:
    ///   - peripheralID: 外设ID
    ///   - characteristicUUID: 特征UUID
    ///   - data: 要写入的数据
    func writeData(peripheralID: UUID, characteristicUUID: CBUUID, data: Data) {
        guard let model = peripheralModels[peripheralID],
              model.state == .connected,
              let characteristics = model.discoveredCharacteristics else {
            print("外设未连接或特征未发现")
            return
        }
        
        guard let char = characteristics.first(where: { $0.uuid == characteristicUUID }) else {
            print("特征不存在")
            return
        }
        
        // 写入方式:WithResponse(需要响应)/ WithoutResponse(无需响应)
        model.peripheral.writeValue(data, for: char, type: .withResponse)
    }
    
    // MARK: - 私有方法
    
    /// 设置连接超时定时器
    /// - Parameter peripheralID: 外设ID
    private func setupConnectTimeoutTimer(for peripheralID: UUID) {
        guard let model = peripheralModels[peripheralID] else { return }
        
        // 先清除旧定时器
        connectTimers[peripheralID]?.invalidate()
        
        // 创建新定时器
        let timer = Timer.scheduledTimer(withTimeInterval: model.connectTimeout, repeats: false) { [weak self] timer in
            guard let self = self else { return }
            
            // 超时处理:断开连接、更新状态
            self.disconnectPeripheral(peripheralID: peripheralID)
            model.state = .failed
            self.onPeripheralStateChanged?(peripheralID, .failed)
            
            // 重连逻辑(如果未达到最大重连次数)
            if model.retryCount < model.maxRetryCount {
                model.retryCount += 1
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    self.connectPeripheral(peripheralID: peripheralID)
                }
            }
            
            timer.invalidate()
            self.connectTimers.removeValue(forKey: peripheralID)
        }
        
        // 加入runloop(否则后台队列的定时器不生效)
        RunLoop.current.add(timer, forMode: .common)
        connectTimers[peripheralID] = timer
    }
}

// MARK: - CBCentralManagerDelegate
extension BLECentralManager: CBCentralManagerDelegate {
    /// 蓝牙状态变化
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .poweredOff:
            print("蓝牙已关闭")
            disconnectAllPeripherals()
            peripheralModels.forEach {
                $0.value.state = .idle
                onPeripheralStateChanged?($0.key, .idle)
            }
        case .poweredOn:
            print("蓝牙已开启")
        case .unauthorized:
            print("蓝牙权限未授权")
        case .unsupported:
            print("设备不支持蓝牙")
        default:
            break
        }
    }
    
    /// 发现外设
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        // 避免重复添加
        if peripheralModels[peripheral.identifier] == nil {
            let model = BLEPeripheralModel(peripheral: peripheral)
            peripheralModels[model.identifier] = model
            print("发现外设:(peripheral.name ?? "未知名称"),ID:(model.identifier)")
        }
    }
    
    /// 外设连接成功
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        guard let model = peripheralModels[peripheral.identifier] else { return }
        
        // 清除超时定时器
        connectTimers[model.identifier]?.invalidate()
        connectTimers.removeValue(forKey: model.identifier)
        
        // 更新状态
        model.state = .connected
        model.retryCount = 0 // 重置重连次数
        onPeripheralStateChanged?(model.identifier, .connected)
        
        // 设置外设代理,发现服务和特征
        peripheral.delegate = self
        peripheral.discoverServices(nil) // nil表示发现所有服务
    }
    
    /// 外设连接失败
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        guard let model = peripheralModels[peripheral.identifier] else { return }
        
        // 清除超时定时器
        connectTimers[model.identifier]?.invalidate()
        connectTimers.removeValue(forKey: model.identifier)
        
        // 更新状态
        model.state = .failed
        onPeripheralStateChanged?(model.identifier, .failed)
        
        print("连接外设失败:(error?.localizedDescription ?? "未知错误"),ID:(model.identifier)")
        
        // 重连逻辑
        if model.retryCount < model.maxRetryCount {
            model.retryCount += 1
            connectQueue.asyncAfter(deadline: .now() + 1) { [weak self] in
                self?.connectPeripheral(peripheralID: model.identifier)
            }
        }
    }
    
    /// 外设断开连接
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        guard let model = peripheralModels[peripheral.identifier] else { return }
        
        // 更新状态
        model.state = .idle
        onPeripheralStateChanged?(model.identifier, .idle)
        
        print("外设断开连接:(error?.localizedDescription ?? "正常断开"),ID:(model.identifier)")
        
        // 可选:自动重连(根据业务需求)
        // connectPeripheral(peripheralID: model.identifier)
    }
}

// MARK: - CBPeripheralDelegate
extension BLECentralManager: CBPeripheralDelegate {
    /// 发现外设服务
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let model = peripheralModels[peripheral.identifier], error == nil else {
            print("发现服务失败:(error?.localizedDescription ?? "未知错误")")
            return
        }
        
        model.discoveredServices = peripheral.services
        print("发现服务:(peripheral.services?.count ?? 0) 个,外设ID:(model.identifier)")
        
        // 发现每个服务下的特征
        peripheral.services?.forEach { service in
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }
    
    /// 发现服务特征
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let model = peripheralModels[peripheral.identifier], error == nil else {
            print("发现特征失败:(error?.localizedDescription ?? "未知错误")")
            return
        }
        
        if model.discoveredCharacteristics == nil {
            model.discoveredCharacteristics = []
        }
        model.discoveredCharacteristics?.append(contentsOf: service.characteristics ?? [])
        
        print("发现特征:(service.characteristics?.count ?? 0) 个,外设ID:(model.identifier)")
        
        // 可选:订阅特征通知(实时接收数据)
        service.characteristics?.forEach { char in
            if char.properties.contains(.notify) {
                peripheral.setNotifyValue(true, for: char)
            }
        }
    }
    
    /// 特征值更新(通知/读取)
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let model = peripheralModels[peripheral.identifier],
              error == nil,
              let data = characteristic.value else {
            print("特征值更新失败:(error?.localizedDescription ?? "无数据")")
            return
        }
        
        // 回调给外部处理数据
        onDataReceived?(model.identifier, data)
        print("收到数据:(data),外设ID:(model.identifier),特征UUID:(characteristic.uuid)")
    }
    
    /// 写入数据响应
    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        guard let model = peripheralModels[peripheral.identifier] else { return }
        
        if let error = error {
            print("写入数据失败:(error.localizedDescription),外设ID:(model.identifier)")
        } else {
            print("写入数据成功,外设ID:(model.identifier),特征UUID:(characteristic.uuid)")
        }
    }
}

3. 使用示例

在 ViewController 中调用管理类:

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 初始化蓝牙管理器
        let bleManager = BLECentralManager.shared
        
        // 监听外设状态变化
        bleManager.onPeripheralStateChanged = { peripheralID, state in
            DispatchQueue.main.async {
                print("外设 (peripheralID) 状态变化:(state)")
                // 更新UI:如连接状态、按钮可点击性等
            }
        }
        
        // 监听数据接收
        bleManager.onDataReceived = { peripheralID, data in
            DispatchQueue.main.async {
                print("收到外设 (peripheralID) 数据:(data)")
                // 解析数据并更新UI
            }
        }
        
        // 开始扫描外设
        bleManager.startScanning()
        
        // 模拟:扫描5秒后连接第一个发现的外设
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
            if let firstPeripheralID = bleManager.peripheralModels.keys.first {
                bleManager.connectPeripheral(peripheralID: firstPeripheralID)
            }
        }
    }
    
    // 断开连接按钮点击事件
    @IBAction func disconnectButtonTapped(_ sender: UIButton) {
        if let firstPeripheralID = BLECentralManager.shared.peripheralModels.keys.first {
            BLECentralManager.shared.disconnectPeripheral(peripheralID: firstPeripheralID)
        }
    }
}

三、关键优化点

1. 性能优化

  • 串行队列管理连接:使用 connectQueue 串行处理连接请求,避免同时发起多个连接导致系统资源耗尽
  • 合理设置扫描参数:避免 CBCentralManagerScanOptionAllowDuplicatesKey = true(重复发现),减少系统开销
  • 自动停止扫描:扫描一段时间后主动停止,降低耗电
  • 批量操作:多个外设的读写操作可分批次执行,避免并发读写导致的数据包丢失

2. 稳定性优化

  • 连接超时处理:每个连接请求设置超时定时器,避免无限等待
  • 断线重连策略:可配置最大重连次数,重连间隔逐步增加(如1s→3s→5s)
  • 状态校验:所有操作前校验蓝牙状态、外设状态,避免无效操作
  • 异常捕获:对读写、连接等操作的错误进行统一处理

3. 资源管理

  • 及时释放资源:断开连接后清除定时器、清空特征缓存
  • 限制最大连接数:iOS 系统对 BLE 同时连接数有上限(约7-8个),需在代码中限制,避免超出系统能力
  • 内存管理:使用 weak self 避免循环引用,防止内存泄漏

四、注意事项

  1. 权限配置:在 Info.plist 中添加蓝牙权限描述:

    1. NSBluetoothAlwaysUsageDescription(iOS 13+)
    2. NSBluetoothPeripheralUsageDescription(iOS 12及以下)
  2. 后台模式:如需后台运行蓝牙,需开启 Info.plist 中的 bluetooth-central 后台模式

  3. 系统限制

    1. iOS 设备同时连接的 BLE 外设数量建议不超过6个
    2. 蓝牙操作必须在后台队列执行,避免阻塞主线程
  4. 测试建议:在真实设备上测试(模拟器不支持 CoreBluetooth)

总结

  1. 多链路管理核心是队列化连接请求 + 状态统一管理 + 异常容错处理,通过单例类封装所有蓝牙操作,对外提供简洁API。
  2. 关键优化点包括:串行处理连接、设置连接超时、断线重连、限制最大连接数,避免系统资源耗尽。
  3. 必须处理蓝牙状态变化、权限、后台模式等系统级问题,确保多链路管理的稳定性和可靠性。