普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月24日首页

iOS Swift:蓝牙 BLE 连接外设CoreBluetooth

作者 tangbin583085
2026年2月24日 17:59

在 iOS 与智能硬件(手环、传感器、控制模块等)交互中,BLE(Bluetooth Low Energy)是最常用的通信方式。本文将基于 CoreBluetooth + Swift,给出一套工程可用的连接外设代码,并总结开发中最常遇到的注意事项。

适用场景:BLE 设备连接、读写特征、订阅通知、接收回包、断线重连。

一、准备工作

1)Info.plist 权限配置(必须)

iOS 13+ 起必须给出蓝牙使用说明,否则扫描/连接会失败或系统拒绝。

<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要使用蓝牙连接外设进行数据通讯</string>

如果你还需要后台持续蓝牙通信:需要 capability + plist 配置(后文注意事项会讲)。

2)导入框架

import CoreBluetooth

二、BLE 连接的标准流程

  1. 初始化 CBCentralManager
  2. 蓝牙开启后开始扫描
  3. 找到目标外设并连接
  4. 发现 Service
  5. 发现 Characteristic
  6. 找到写特征(Write)与通知特征(Notify)
  7. 开启通知并开始收发数据

三、代码:BluetoothManager(可直接用)

下面给出一个轻量但工程化的 BLE 管理器,支持:

  • 按名称/服务 UUID 过滤
  • 扫描超时
  • 连接成功后自动发现服务/特征
  • 自动开启 Notify
  • 写入数据(支持写入响应/无响应)
  • 断开回调(可扩展重连)
  • 简单的写入节流(避免写太快导致丢包) ``

你只需要把 UUID 替换成你设备的即可。

import Foundation
import CoreBluetooth

final class BluetoothManager: NSObject {
    
    static let shared = BluetoothManager()
    
    // MARK: - Public callbacks (按需扩展)
    var onStateChanged: ((CBManagerState) -> Void)?
    var onDiscovered: ((CBPeripheral, NSNumber) -> Void)?
    var onConnected: ((CBPeripheral) -> Void)?
    var onDisconnected: ((CBPeripheral, Error?) -> Void)?
    var onReceiveData: ((Data, CBCharacteristic) -> Void)?
    
    // MARK: - CoreBluetooth
    private var central: CBCentralManager!
    private(set) var peripheral: CBPeripheral?
    
    private var writeChar: CBCharacteristic?
    private var notifyChar: CBCharacteristic?
    
    // MARK: - Config (替换为你的设备 UUID)
    /// 推荐:用 Service UUID 过滤扫描,效率更高、结果更准
    private let targetServiceUUID = CBUUID(string: "FFF0")
    private let writeCharUUID      = CBUUID(string: "FFF1")
    private let notifyCharUUID     = CBUUID(string: "FFF2")
    
    /// 可选:按名称过滤(如果设备名称稳定)
    private let targetNamePrefix = "MyBLE"
    
    // MARK: - Scan control
    private var scanTimer: Timer?
    private let scanTimeout: TimeInterval = 10
    
    // MARK: - Write throttle
    private var writeQueue: [Data] = []
    private var isWriting = false
    
    private override init() {
        super.init()
        // queue 建议用串行队列,避免回调并发导致状态错乱
        let queue = DispatchQueue(label: "com.tangbin.ble.queue")
        central = CBCentralManager(delegate: self, queue: queue)
    }
    
    // MARK: - Public APIs
    
    /// 开始扫描
    func startScan() {
        guard central.state == .poweredOn else { return }
        stopScan()
        
        // 只扫目标 Service:更省电更精准(强烈推荐)
        central.scanForPeripherals(withServices: [targetServiceUUID], options: [
            CBCentralManagerScanOptionAllowDuplicatesKey: false
        ])
        
        startScanTimeoutTimer()
    }
    
    /// 停止扫描
    func stopScan() {
        if central.isScanning {
            central.stopScan()
        }
        scanTimer?.invalidate()
        scanTimer = nil
    }
    
    /// 连接外设
    func connect(_ p: CBPeripheral) {
        stopScan()
        peripheral = p
        peripheral?.delegate = self
        
        central.connect(p, options: [
            CBConnectPeripheralOptionNotifyOnDisconnectionKey: true
        ])
    }
    
    /// 主动断开
    func disconnect() {
        guard let p = peripheral else { return }
        central.cancelPeripheralConnection(p)
    }
    
    /// 发送数据(写入队列节流)
    func send(_ data: Data, withResponse: Bool = false) {
        guard let p = peripheral, let w = writeChar else { return }
        let type: CBCharacteristicWriteType = withResponse ? .withResponse : .withoutResponse
        
        // 如果写入无响应,也建议做节流,避免外设来不及处理
        writeQueue.append(data)
        pumpWriteQueue(peripheral: p, characteristic: w, type: type)
    }
    
    // MARK: - Private
    
    private func startScanTimeoutTimer() {
        scanTimer?.invalidate()
        scanTimer = Timer.scheduledTimer(withTimeInterval: scanTimeout, repeats: false) { [weak self] _ in
            self?.stopScan()
        }
        RunLoop.main.add(scanTimer!, forMode: .common)
    }
    
    private func pumpWriteQueue(peripheral p: CBPeripheral,
                                characteristic c: CBCharacteristic,
                                type: CBCharacteristicWriteType) {
        guard !isWriting else { return }
        guard !writeQueue.isEmpty else { return }
        
        isWriting = true
        let packet = writeQueue.removeFirst()
        
        // 注意:此处写入发生在 central 的队列上
        p.writeValue(packet, for: c, type: type)
        
        // withoutResponse 的情况下不会走 didWriteValueFor,所以用延迟释放
        if type == .withoutResponse {
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.02) { [weak self] in
                self?.isWriting = false
                self?.pumpWriteQueue(peripheral: p, characteristic: c, type: type)
            }
        }
    }
}

// MARK: - CBCentralManagerDelegate
extension BluetoothManager: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        onStateChanged?(central.state)
        
        if central.state == .poweredOn {
            // 可按需自动扫描
            // startScan()
        } else {
            // 蓝牙关闭/不可用时要清空状态
            stopScan()
            peripheral = nil
            writeChar = nil
            notifyChar = nil
        }
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String : Any],
                        rssi RSSI: NSNumber) {
        
        // 如果还想按名称做二次过滤
        if let name = peripheral.name, name.hasPrefix(targetNamePrefix) {
            onDiscovered?(peripheral, RSSI)
            connect(peripheral)
            return
        }
        
        // 不按名称过滤也行:只要服务 UUID 已经过滤,通常就很准
        onDiscovered?(peripheral, RSSI)
        connect(peripheral)
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        onConnected?(peripheral)
        
        // 连接后发现服务
        peripheral.discoverServices([targetServiceUUID])
    }
    
    func centralManager(_ central: CBCentralManager,
                        didFailToConnect peripheral: CBPeripheral,
                        error: Error?) {
        onDisconnected?(peripheral, error)
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDisconnectPeripheral peripheral: CBPeripheral,
                        error: Error?) {
        onDisconnected?(peripheral, error)
        
        // 可选:重连策略(示例:延迟重连)
        // DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        //     central.connect(peripheral, options: nil)
        // }
    }
}

// MARK: - CBPeripheralDelegate
extension BluetoothManager: CBPeripheralDelegate {
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard error == nil else { return }
        guard let services = peripheral.services else { return }
        
        for s in services {
            if s.uuid == targetServiceUUID {
                peripheral.discoverCharacteristics([writeCharUUID, notifyCharUUID], for: s)
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didDiscoverCharacteristicsFor service: CBService,
                    error: Error?) {
        guard error == nil else { return }
        guard let chars = service.characteristics else { return }
        
        for c in chars {
            if c.uuid == writeCharUUID { writeChar = c }
            if c.uuid == notifyCharUUID { notifyChar = c }
        }
        
        // 开启通知(Notify)
        if let n = notifyChar {
            peripheral.setNotifyValue(true, for: n)
        }
        
        // 可选:读取一次初始值
        // if let n = notifyChar { peripheral.readValue(for: n) }
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didUpdateNotificationStateFor characteristic: CBCharacteristic,
                    error: Error?) {
        // 通知开关状态回调
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didUpdateValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        guard error == nil else { return }
        let data = characteristic.value ?? Data()
        onReceiveData?(data, characteristic)
    }
    
    func peripheral(_ peripheral: CBPeripheral,
                    didWriteValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        // 只有 withResponse 才会进这个回调
        isWriting = false
        if let w = writeChar {
            pumpWriteQueue(peripheral: peripheral, characteristic: w, type: .withResponse)
        }
    }
}

四、调用示例(在 ViewController 里)

final class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let ble = BluetoothManager.shared
        
        ble.onStateChanged = { state in
            print("BLE state:", state.rawValue)
            if state == .poweredOn {
                ble.startScan()
            }
        }
        
        ble.onConnected = { p in
            print("已连接:", p.name ?? "unknown")
        }
        
        ble.onReceiveData = { data, ch in
            let hex = data.map { String(format: "%02x", $0) }.joined()
            print("收到数据((ch.uuid)):", hex)
        }
    }
    
    func sendCommand() {
        // 举例:发送一段 hex 指令
        let hex = "aabbccdd"
        let data = Data(hexString: hex) // 见下方扩展
        BluetoothManager.shared.send(data, withResponse: false)
    }
}

也可以复用我之前写的 ParseDataTool(Swift版)来做 Hex/Data 转换。


五、附:Data 十六进制扩展(可选)

extension Data {
    init(hexString: String) {
        let clean = hexString.replacingOccurrences(of: " ", with: "")
        var data = Data()
        var idx = clean.startIndex
        while idx < clean.endIndex {
            let next = clean.index(idx, offsetBy: 2)
            let byteStr = clean[idx..<next]
            if let b = UInt8(byteStr, radix: 16) {
                data.append(b)
            }
            idx = next
        }
        self = data
    }
}

六、开发注意事项(非常关键)

1)强烈建议:扫描时指定 Service UUID

更快、更省电、更准确
避免扫描全部 nil 会扫到大量无关设备,影响体验

central.scanForPeripherals(withServices: [targetServiceUUID], options: nil)

2)不要用 peripheral.name 当唯一标识

name 可能为空、可能变化。更靠谱的是:

  • 过滤 Service UUID
  • 使用 peripheral.identifier(UUID)做缓存识别(重连)

3)写数据别太快,否则丢包/外设卡死

BLE 写入速度过快常见问题:

  • 外设缓冲区溢出
  • 回包延迟或丢失
  • iOS 侧 write 被吞

建议做写入节流(本文示例已经做了队列 + 20ms 间隔)


4)区分 withResponse / withoutResponse

  • .withResponse:可靠,有回调 didWriteValueFor
  • .withoutResponse:速度快,但无写入确认,建议配合队列节流

实战建议:

  • 协议关键指令用 .withResponse
  • 大数据(如 OTA)用 .withoutResponse + 节流 + 外设 ACK

5)Notify 要记得开启(很多人漏掉)

外设回包多数走 Notify(通知),不打开你永远收不到数据:

peripheral.setNotifyValue(true, for: notifyChar)

6)断线是常态:要做重连策略

断线原因很多:

  • 超距
  • 外设省电休眠
  • 手机锁屏/系统资源回收

建议:

  • didDisconnectPeripheral 里做延迟重连
  • 或 UI 提示用户手动重连
  • 结合 peripheral.identifier 记住上次设备

7)后台蓝牙通信(可选)

如果你需要锁屏/后台持续通信:

  • Xcode → Signing & Capabilities → Background Modes → 勾选 Uses Bluetooth LE accessories
  • 并合理控制扫描/连接行为(后台会更耗电)

8)MTU 与分包问题

  • BLE 默认有效载荷常见为 20 字节(不同设备协商后可能变大)
  • 大数据(日志、图片、OTA)一定要做分包 + 协议确认

最后

本文给出了一套 Swift BLE 连接外设的我开发成熟项目过程中的代码,可直接运用在项目中,覆盖了:

  • 初始化、扫描、连接
  • 服务/特征发现
  • Notify 开启、收包回调
  • 写入(带队列节流)
  • 断开处理与重连扩展

如有写错的地方,敬请指正,相互学习进步,谢谢~

❌
❌