在BLE(低功耗蓝牙)开发中,广播包是设备实现“被动发现”的核心载体——智能硬件、工业模块、可穿戴设备等,均需通过广播包向周边扫描设备(手机、网关等)传递自身身份与基础信息,供其识别、筛选并建立连接。广播包的核心组成是Advertising Data(广播数据)Scan Response(扫描响应数据),二者结构遵循GAP协议规范,但交互逻辑、使用场景存在明确差异,也是开发中“设备扫不到”“数据解析异常”“自定义字段失效”等问题的核心诱因。

本文将从底层协议规范出发,拆解两个字段的核心结构、AD单元定义及高频类型,结合iOS(Swift)、Android(Kotlin)两种主流原生技术栈的实战代码,聚焦字段配置、解析逻辑、权限适配及避坑要点,兼顾理论严谨性与实战可复用性,助力开发者彻底掌握BLE广播包开发的核心逻辑,避开常见踩坑点。

一、前置认知:BLE广播包的核心规范(必记)

BLE广播包由广播者(Advertiser)周期性发送,遵循GAP(通用访问规范)协议约束,核心作用是传递设备基础信息,无需扫描者主动请求即可被动接收。其核心规范直接决定广播包的有效性,需重点掌握:

  • 长度约束(硬性):Advertising Data与Scan Response的总长度均不得超过31字节,超出会直接导致广播失败(蓝牙芯片会拒绝发送违规数据包)。

  • 结构统一:二者均由若干个“AD单元”组成,单个AD单元严格遵循「1字节Length + 1字节AD Type + N字节AD Data」的结构,无例外。

  • 交互逻辑:Advertising Data为必选字段,广播者主动周期性发送;Scan Response为可选字段,仅在扫描者发送Scan Request后被动响应,用于承载非核心补充信息。

  • 单元约束:单个广播包的AD单元数量建议不超过5个(部分iOS设备及低功耗蓝牙芯片有明确限制),避免单元过多导致解析异常。

补充:Advertising Data与Scan Response的长度独立计算,互不占用对方字节空间,可根据需求拆分核心与非核心信息,避免单字段超出31字节限制。

二、核心拆解:AD单元结构与高频类型详解

AD单元是Advertising Data与Scan Response的最小组成单元,三者(Length、AD Type、AD Data)的配合直接决定数据的有效性与可解析性,以下结合实战场景拆解各部分核心要点。

(一)Length:AD单元的尺寸标识(1字节)

Length字段的核心作用是标识当前AD单元中「AD Type + AD Data」的总字节数,取值范围为1~30(十进制),核心计算逻辑为:

$$\\text{Length} = 1\\ (\\text{AD Type字节数}) + N\\ (\\text{AD Data字节数})$$,即 $$N = \\text{Length} - 1$$

避坑要点:Length不可为0(无意义),也不可超过30(否则AD单元总长度会超出广播包31字节限制);多AD单元拼接时,需确保所有单元总长度≤31字节。

示例:AD Type=0x09(完整设备名称),AD Data为“Smart-Band”(7字节),则Length=1+7=8(十进制0x08),AD单元为「0x08 + 0x09 + 0x53 0x6D 0x61 0x72 0x74 0x2D 0x42 0x61 0x6E 0x64」。

(二)AD Type:数据的身份标识(1字节)

AD Type取值为0x00~0xFF(十六进制),由BLE协议规范定义,用于指定后续AD Data的含义与格式,开发中高频使用的类型(按使用率排序)如下,精准对应实战场景:

AD Type(十六进制)

类型名称

实战作用

AD Data格式要求

0x03

Complete List of 16-bit Service UUIDs

核心筛选字段,用于扫描者精准匹配目标设备

2字节/个UUID,多个连续拼接(如0xFFE0对应UUID:0000FFE0-0000-1000-8000-00805F9B34FB)

0x09

Complete Local Name

设备直观标识,便于用户识别(如“Smart-Watch-01”)

UTF-8编码字符串,长度≤30字节(需结合Length计算)

0x0A

Manufacturer Specific Data

厂商自定义字段,传递固件版本、序列号等私有信息

前2字节为蓝牙SIG分配的厂商ID,后续为自定义数据

0x11

Tx Power Level

辅助判断设备距离,扫描者根据功率计算信号衰减

1字节,取值范围-40~+4 dBm(十进制)

0x01

Flags

标识设备基础属性,决定是否可连接、是否支持BLE

1字节,常用值0x06(可连接、仅支持BLE,不支持经典蓝牙)

(三)AD Data:核心信息载体(N字节)

AD Data的格式、长度完全由对应AD Type决定,是广播包的核心内容,结合高频AD Type给出实战示例(直接复用):

  1. AD Type=0x09(完整设备名称):Length=8,AD Data=0x53 0x6D 0x61 0x72 0x74 0x2D 0x42 0x61 0x6E 0x64(对应“Smart-Band”);

  2. AD Type=0x03(16位服务UUID):Length=3,AD Data=0xFF 0xE0(对应服务UUID:0000FFE0-0000-1000-8000-00805F9B34FB);

  3. AD Type=0x0A(厂商自定义数据):Length=5,AD Data=0x00 0x01 0x02 0x05(厂商ID=0x0001,固件版本V2.05);

  4. AD Type=0x11(发射功率):Length=2,AD Data=0x00(对应发射功率0 dBm)。

(四)Advertising Data与Scan Response的核心差异(实战必区分)

二者结构完全一致,但交互逻辑、使用场景差异显著,直接影响开发中的字段分配,具体对比如下:

对比维度

Advertising Data(广播数据)

Scan Response(扫描响应数据)

交互方式

主动周期性发送,扫描者被动接收,无需请求

被动响应,仅在扫描者发送Scan Request后触发

核心作用

传递核心信息(设备名称、服务UUID),供扫描者快速筛选

传递补充信息(固件版本、序列号),节省广播数据空间

必要性

必选(无则设备无法被扫描发现)

可选(无补充信息可不配,不影响设备发现)

优先级

高(优先分配字节,确保核心信息传递)

低(仅在广播数据有剩余需求时配置)

三、实战落地:iOS、Android原生完整实现(可直接复用)

结合协议规范,以下给出iOS(Swift)、Android(Kotlin)两种原生技术栈的广播者(配置)与扫描者(解析)完整代码,聚焦实战细节、权限适配及异常处理,规避常见坑点,代码可直接复制到项目中联动调试。

(一)iOS 原生(Swift):基于CoreBluetooth框架

iOS通过CoreBluetooth框架封装BLE底层接口,不允许手动拼接AD单元,需通过框架提供的键值对配置,底层自动遵循GAP协议,核心需关注权限配置与状态监听。

1. 广播者:Advertising Data与Scan Response配置
import CoreBluetooth

class iOSBLEAdvertiser: NSObject, CBPeripheralManagerDelegate {
    // 广播核心管理器,负责广播配置与状态回调
    private var peripheralManager: CBPeripheralManager!
    // 广播数据与扫描响应数据(严格遵循31字节限制)
    private var advertisingData: [String: Any] = [:]
    private var scanResponseData: [String: Any] = [:]
    // 目标服务UUID(与扫描者统一,确保精准匹配)
    private let serviceUUID = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB")
    
    override init() {
        super.init()
        // 初始化广播管理器,主线程回调(确保UI更新安全)
        peripheralManager = CBPeripheralManager(delegate: self, queue: DispatchQueue.main)
    }
    
    /// 配置广播包(核心:按AD Type对应框架键值对,避免手动拼接)
    private func configAdvertising() {
        // 1. 配置Advertising Data(必选,核心信息)
        advertisingData = [
            CBAdvertisementDataLocalNameKey: "iOS-BLE-Test", // AD Type=0x09(设备名称)
            CBAdvertisementDataServiceUUIDsKey: [serviceUUID], // AD Type=0x03(服务UUID)
            CBAdvertisementDataTxPowerLevelKey: 0 as NSNumber, // AD Type=0x11(发射功率0dBm)
            CBAdvertisementDataFlagsKey: 0x06 // AD Type=0x01(可连接、仅支持BLE)
        ]
        
        // 2. 配置Scan Response(可选,补充厂商自定义数据)
        let manufacturerID: UInt16 = 0x0001 // 蓝牙SIG分配厂商ID
        let firmwareVersion = Data([0x01, 0x02]) // 自定义固件版本V1.02
        // 厂商数据格式:前2字节厂商ID + 后续自定义数据(AD Type=0x0A)
        let manufacturerData = Data(bytes: &manufacturerID, count: MemoryLayout<UInt16>.size) + firmwareVersion
        scanResponseData = [CBAdvertisementDataManufacturerDataKey: manufacturerData]
    }
    
    /// 启动广播(需先校验蓝牙状态与权限)
    func startAdvertising() {
        guard peripheralManager.state == .poweredOn else {
            print("蓝牙未开启,无法启动广播")
            return
        }
        configAdvertising()
        // 启动广播,传入广播数据与扫描响应数据
        peripheralManager.startAdvertising(advertisingData)
    }
    
    /// 停止广播,释放资源
    func stopAdvertising() {
        if peripheralManager.isAdvertising {
            peripheralManager.stopAdvertising()
            print("iOS BLE广播已停止")
        }
    }
    
    // MARK: - CBPeripheralManagerDelegate
    /// 蓝牙状态变化回调(核心:监听蓝牙开关状态)
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        switch peripheral.state {
        case .poweredOn:
            startAdvertising()
            print("iOS BLE广播启动成功,配置:\(advertisingData)")
        case .poweredOff:
            stopAdvertising()
            print("蓝牙已关闭,广播停止")
        case .unauthorized:
            print("蓝牙权限未授予,无法启动广播")
        default:
            print("广播不可用,状态:\(peripheral.state.rawValue)")
        }
    }
    
    /// 广播启动失败回调(捕捉字段配置错误,如长度超出)
    func peripheralManager(_ peripheral: CBPeripheralManager, didFailToStartAdvertising error: Error?) {
        if let error = error {
            print("广播启动失败(配置异常):\(error.localizedDescription)")
        }
    }
}

// 使用示例
// let advertiser = iOSBLEAdvertiser()
2. 扫描者:Advertising Data与Scan Response解析
import CoreBluetooth

class iOSBLEScanner: NSObject, CBCentralManagerDelegate {
    private var centralManager: CBCentralManager!
    // 目标服务UUID(与广播者统一,实现精准筛选)
    private let targetServiceUUID = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB")
    
    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main)
    }
    
    /// 启动扫描(校验蓝牙状态、权限,精准筛选目标设备)
    func startScanning() {
        guard centralManager.state == .poweredOn else {
            print("蓝牙未开启,无法启动扫描")
            return
        }
        // iOS 13+ 需始终允许蓝牙权限(否则无法扫描到设备)
        guard centralManager.authorization == .allowedAlways else {
            centralManager.requestAlwaysAuthorization()
            print("请授予蓝牙始终允许权限")
            return
        }
        // 仅扫描携带目标服务UUID的设备,提升扫描效率
        centralManager.scanForPeripherals(withServices: [targetServiceUUID], options: nil)
        print("iOS BLE扫描启动,开始解析广播包...")
    }
    
    /// 停止扫描,避免资源浪费
    func stopScanning() {
        if centralManager.isScanning {
            centralManager.stopScan()
            print("iOS BLE扫描已停止")
        }
    }
    
    // MARK: - CBCentralManagerDelegate
    /// 发现设备回调(核心:解析广播包数据)
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        print("=== 发现设备:\(peripheral.name ?? "未知设备")(地址:\(peripheral.identifier.uuidString))===")
        
        // 1. 解析Advertising Data(核心基础信息)
        // 设备名称(AD Type=0x09,对应CBAdvertisementDataLocalNameKey)
        if let deviceName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
            print("Advertising Data - 设备名称:\(deviceName)")
        }
        // 服务UUID(AD Type=0x03,对应CBAdvertisementDataServiceUUIDsKey)
        if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
            let uuidStr = serviceUUIDs.map { $0.uuidString }.joined(separator: ", ")
            print("Advertising Data - 服务UUID:\(uuidStr)")
        }
        // 发射功率(AD Type=0x11,对应CBAdvertisementDataTxPowerLevelKey)
        if let txPower = advertisementData[CBAdvertisementDataTxPowerLevelKey] as? NSNumber {
            print("Advertising Data - 发射功率:\(txPower) dBm")
        }
        
        // 2. 解析Scan Response(补充信息,厂商自定义数据)
        if let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
            // 前2字节为厂商ID,后续为自定义数据(AD Type=0x0A)
            guard manufacturerData.count > 2 else {
                print("Scan Response - 厂商数据格式错误")
                return
            }
            let manufacturerID = manufacturerData.subdata(in: 0..<2).withUnsafeBytes { $0.load(as: UInt16.self) }
            let firmwareData = manufacturerData.subdata(in: 2..<manufacturerData.count)
            let firmwareVersion = "V\(firmwareData[0]).\(firmwareData[1])"
            print("Scan Response - 厂商ID:\(manufacturerID),固件版本:\(firmwareVersion)")
        }
        
        // 信号强度(辅助判断设备距离,负值越小信号越强)
        print("设备RSSI:\(RSSI) dBm\n")
    }
    
    /// 蓝牙状态变化回调
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            startScanning()
        } else {
            stopScanning()
        }
    }
}

// 使用示例
// let scanner = iOSBLEScanner()
iOS实战避坑要点
  • 权限配置:Info.plist必须添加「NSBluetoothAlwaysUsageDescription」(始终允许蓝牙权限),否则无法通过App Store审核,且广播/扫描会失败。

  • 字段限制:禁止手动拼接AD单元字节,必须使用CoreBluetooth提供的键值对(如CBAdvertisementDataLocalNameKey对应AD Type=0x09),否则解析异常。

  • 长度管控:设备名称过长(超过20字节)时,建议拆分至Scan Response,避免Advertising Data超出31字节限制。

(二)Android 原生(Kotlin):基于BluetoothLeAdvertiser

Android对BLE广播的灵活性更高,支持Builder模式快速配置与手动拼接AD单元两种方式,核心需关注系统版本权限适配(Android 12+细粒度权限)与解析逻辑的严谨性。

1. 广播者:Advertising Data与Scan Response配置
import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat

class AndroidBLEAdvertiser(private val context: Context) {
    // 蓝牙核心组件(适配器、广播管理器)
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    private var bleAdvertiser: BluetoothLeAdvertiser? = null
    // 目标服务UUID(与扫描者统一)
    private val serviceUUID = android.bluetooth.UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB")
    
    /// 初始化广播配置(校验蓝牙可用性、配置广播参数)
    fun initAdvertiser() {
        if (bluetoothAdapter == null) {
            println("设备不支持BLE功能")
            return
        }
        if (!bluetoothAdapter.isEnabled) {
            println("蓝牙未开启,建议先开启蓝牙")
            return
        }
        // 获取广播管理器(部分设备不支持BLE广播,需判空)
        bleAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser ?: run {
            println("设备不支持BLE广播功能")
            return
        }
    }
    
    /// 启动广播(核心:权限适配、字段配置、状态监听)
    fun startAdvertising() {
        if (bleAdvertiser == null) {
            println("广播未初始化,请先调用initAdvertiser()")
            return
        }
        // 权限适配(Android 12+ 拆分蓝牙权限,广播需单独申请BLUETOOTH_ADVERTISE)
        val requiredPermission = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
            Manifest.permission.BLUETOOTH_ADVERTISE
        } else {
            Manifest.permission.BLUETOOTH_ADMIN
        }
        if (ActivityCompat.checkSelfPermission(context, requiredPermission) != PackageManager.PERMISSION_GRANTED) {
            println("缺少广播权限,无法启动广播")
            return
        }
        
        // 1. 配置广播设置(与字段无关,控制广播功率、模式)
        val advertiseSettings = AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED) // 平衡功耗与扫描效率
            .setConnectable(true) // 可连接广播(允许扫描者发起连接)
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM) // 中等发射功率
            .setTimeout(0) // 无超时,持续广播
            .build()
        
        // 2. 配置Advertising Data(Builder模式,简洁高效)
        val advertisingData = AdvertiseData.Builder()
            .setIncludeDeviceName(true) // 包含设备名称(AD Type=0x09)
            .addServiceUuid(android.bluetooth.UUID(serviceUUID)) // AD Type=0x03(服务UUID)
            .setIncludeTxPowerLevel(true) // 包含发射功率(AD Type=0x11)
            .build()
        
        // 3. 配置Scan Response(手动拼接AD单元,灵活自定义,AD Type=0x0A)
        // 手动拼接格式:Length(0x05) + AD Type(0x0A) + 厂商ID(0x0001) + 固件版本(0x0304)
        val scanResponseData = AdvertiseData.Builder()
            .addManufacturerData(0x0001, byteArrayOf(0x03, 0x04)) // 厂商ID+固件版本V3.04
            .build()
        
        // 启动广播,监听回调
        bleAdvertiser?.startAdvertising(advertiseSettings, advertisingData, scanResponseData, advertiseCallback)
    }
    
    /// 停止广播,释放资源
    fun stopAdvertising() {
        bleAdvertiser?.stopAdvertising(advertiseCallback)
        println("Android BLE广播已停止")
    }
    
    // 广播状态回调(捕捉启动成功/失败)
    private val advertiseCallback = object : AdvertiseCallback() {
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
            super.onStartSuccess(settingsInEffect)
            println("Android BLE广播启动成功")
        }
        
        override fun onStartFailure(errorCode: Int) {
            super.onStartFailure(errorCode)
            val errorMsg = when (errorCode) {
                ADVERTISE_FAILED_DATA_TOO_LARGE -> "广播数据超出31字节限制(字段配置错误)"
                ADVERTISE_FAILED_ALREADY_STARTED -> "广播已启动,无需重复调用"
                ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "设备不支持当前广播模式"
                else -> "广播失败,错误码:$errorCode"
            }
            println("广播启动失败:$errorMsg")
        }
    }
}

// 使用示例
// val advertiser = AndroidBLEAdvertiser(context)
// advertiser.initAdvertiser()
// advertiser.startAdvertising()
2. 扫描者:Advertising Data与Scan Response解析
import android.Manifest
import android.bluetooth.BluetoothManager
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat

class AndroidBLEScanner(private val context: Context) {
    // 蓝牙核心组件(管理器、适配器、扫描器)
    private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    private val bluetoothAdapter = bluetoothManager.adapter
    private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
    // 目标服务UUID(与广播者统一,精准筛选)
    private val targetServiceUUID = android.bluetooth.UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB")
    
    /// 启动扫描(权限适配、蓝牙校验、筛选配置)
    fun startScanning() {
        if (bluetoothAdapter == null) {
            println("设备不支持BLE功能")
            return
        }
        if (!bluetoothAdapter.isEnabled) {
            println("蓝牙未开启,无法启动扫描")
            return
        }
        if (bleScanner == null) {
            println("设备不支持BLE扫描功能")
            return
        }
        // 权限适配(Android 12+ 需申请BLUETOOTH_SCAN,低版本需定位权限)
        val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(Manifest.permission.BLUETOOTH_SCAN)
        } else {
            arrayOf(Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_FINE_LOCATION)
        }
        if (requiredPermissions.any { ActivityCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED }) {
            println("缺少扫描权限,无法启动扫描")
            return
        }
        
        // 配置扫描过滤(仅扫描携带目标服务UUID的设备,提升效率)
        val scanFilter = ScanFilter.Builder()
            .setServiceUuid(android.bluetooth.UUID(targetServiceUUID))
            .build()
        val scanFilters = listOf(scanFilter)
        
        // 启动扫描,解析广播包(回调在子线程,UI更新需切换主线程)
        bleScanner.startScan(scanFilters, null, scanCallback)
        println("Android BLE扫描启动,开始解析广播包...")
    }
    
    /// 停止扫描,释放资源
    fun stopScanning() {
        bleScanner?.stopScan(scanCallback)
        println("Android BLE扫描已停止")
    }
    
    // 扫描回调(核心:解析广播包与扫描响应数据)
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)
            val device = result.device
            println("=== 发现设备:${device.name ?: "未知设备"}(地址:${device.address})===")
            
            // 1. 解析Advertising Data(result.advertisementData直接获取)
            val advertisingData = result.advertisementData
            // 设备名称(AD Type=0x09)
            advertisingData.deviceName?.let {
                println("Advertising Data - 设备名称:$it")
            }
            // 服务UUID(AD Type=0x03)
            advertisingData.serviceUuids?.let { uuids ->
                val uuidStr = uuids.map { it.uuid.toString() }.joinToString(", ")
                println("Advertising Data - 服务UUID:$uuidStr")
            }
            // 发射功率(AD Type=0x11,Int.MIN_VALUE表示未配置)
            if (advertisingData.txPowerLevel != Int.MIN_VALUE) {
                println("Advertising Data - 发射功率:${advertisingData.txPowerLevel} dBm")
            }
            
            // 2. 解析Scan Response(result.scanRecord?.scanResponseData获取)
            val scanResponseData = result.scanRecord?.scanResponseData
            scanResponseData?.let { data ->
                parseScanResponse(data)
            }
            
            // 信号强度(辅助判断设备距离)
            println("设备RSSI:${result.rssi} dBm(负值越小,信号越强)\n")
        }
        
        /// 手动解析Scan Response的AD单元(适配自定义字段,通用逻辑)
        private fun parseScanResponse(data: ByteArray) {
            var index = 0
            while (index < data.size) {
                val length = data[index].toInt() // Length字段(AD Type + AD Data总长度)
                // 无效AD单元(长度为0或超出数据范围),退出循环
                if (length == 0 || index + length >= data.size) break
                
                val type = data[index + 1].toInt() // AD Type字段
                val adData = data.copyOfRange(index + 2, index + 1 + length) // AD Data字段
                
                // 解析厂商自定义数据(AD Type=0x0A)
                if (type == 0x0A) {
                    // 厂商数据格式:前2字节厂商ID + 后续自定义数据
                    if (adData.size >= 2) {
                        val manufacturerID = (adData[0].toInt() and 0xFF) or ((adData[1].toInt() and 0xFF) shl 8)
                        val firmwareVersion = "V${adData[2]}.${adData[3]}"
                        println("Scan Response - 厂商ID:$manufacturerID,固件版本:$firmwareVersion")
                    }
                }
                
                index += length + 1 // 移动到下一个AD单元
            }
        }
        
        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            println("扫描失败,错误码:$errorCode(1=蓝牙未开启,2=权限不足,3=设备不支持)")
        }
    }
}

// 使用示例
// val scanner = AndroidBLEScanner(context)
// scanner.startScanning()
Android实战避坑要点
  • 权限适配:Android 12+ 拆分蓝牙权限,广播需申请「BLUETOOTH_ADVERTISE」,扫描需申请「BLUETOOTH_SCAN」;Android 10及以下扫描需额外申请「ACCESS_FINE_LOCATION」,否则无法扫描到设备。

  • 配置方式:Builder模式适合常用字段(设备名称、服务UUID),手动拼接适合自定义AD Type,拼接时需严格遵循「Length+Type+Data」结构,避免字节错位。

  • 解析逻辑:Scan Response需通过「result.scanRecord?.scanResponseData」获取,手动解析时需循环遍历AD单元,通过Length判断单元边界,避免遗漏或解析错误。

  • 兼容性:Android 7.0及以下设备对广播包长度限制更严格,建议单个AD单元Length不超过20字节,避免系统拒绝发送。

四、总结:核心要点与实战建议

BLE广播包的核心是Advertising Data与Scan Response的合理配置与解析,二者遵循统一的AD单元结构,但分工明确——前者承载核心识别信息,后者补充私有补充信息,共同实现设备的“被动发现”与“信息传递”。

对于iOS开发者,需重点关注CoreBluetooth框架的规范使用,避免手动拼接AD单元,做好权限配置与状态监听;对于Android开发者,可灵活选择配置方式,重点处理权限适配与解析逻辑的严谨性,兼顾不同系统版本的兼容性。

实战中,建议优先将核心信息(服务UUID、设备名称)放入Advertising Data,非核心信息(固件版本、序列号)放入Scan Response,严格控制单字段长度不超过31字节,同时做好异常处理(广播失败、扫描失败),可大幅降低开发踩坑概率。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐