Android 蓝牙BLE开发 实战指南(从零到一)
1. 认识BLE蓝牙技术第一次接触BLE开发时我盯着手机上的智能手环数据同步功能发呆——这个小玩意儿怎么做到每天只充一次电却能24小时不间断监测心率答案就藏在BLEBluetooth Low Energy技术中。和传统蓝牙相比BLE就像个精打细算的管家只在需要通信时短暂唤醒设备其他时间都保持深度睡眠状态。BLE的独特优势在实际项目中尤为明显。去年给健身房开发体脂秤时传统蓝牙方案让设备续航从3个月骤降到2周换成BLE后直接延长到18个月。这种低功耗特性源于两种技术架构的本质差异经典蓝牙保持长连接持续耗电而BLE采用快速传输立即休眠机制平均功耗仅为前者的1/10。典型应用场景随处可见健康监测类心率带、血糖仪等医疗设备运动追踪类智能跑鞋、运动手环智能家居类电子门锁、温湿度传感器位置服务类室内导航信标在Android生态中BLE支持从Android 4.3API 18开始引入但直到Android 8.0才真正成熟。现在连百元机都标配BLE 5.0理论传输距离可达300米实际室内约30-50米。最近给物流公司做的仓库盘点项目中就利用这个特性实现了多标签同时扫描。2. 开发环境与权限配置去年适配Android 12时我被新权限系统坑得不轻——明明扫描代码没问题但就是搜不到设备。后来发现是忘了处理新的BLUETOOTH_SCAN权限。现在每次新建BLE项目我都会先配置好这些基础项AndroidManifest.xml需要包含这些权限!-- 基础蓝牙权限 -- uses-permission android:nameandroid.permission.BLUETOOTH/ uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN/ !-- Android 6.0 需要位置权限 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ !-- Android 10 后台扫描需要 -- uses-permission android:nameandroid.permission.ACCESS_BACKGROUND_LOCATION/ !-- Android 12 新增的BLE权限 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation tools:targetApis/ uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT/运行时权限请求有个小技巧用组合请求减少用户打扰。我通常会这样处理private fun checkPermissions() { val neededPermissions mutableListOfString() if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { neededPermissions.add(Manifest.permission.BLUETOOTH_SCAN) neededPermissions.add(Manifest.permission.BLUETOOTH_CONNECT) } else { neededPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION) } if (neededPermissions.any { ContextCompat.checkSelfPermission(this, it) ! PackageManager.PERMISSION_GRANTED }) { ActivityCompat.requestPermissions( this, neededPermissions.toTypedArray(), REQUEST_CODE_BLE_PERMISSIONS ) } }蓝牙适配器检查是很多人容易忽略的步骤。有次客户反馈App闪退最后发现是红米Note 9 Pro阉割了BLE模块。现在我的代码里一定会加这个检查val bluetoothManager getSystemService(BLUETOOTH_SERVICE) as BluetoothManager val bluetoothAdapter bluetoothManager.adapter ?: run { Toast.makeText(this, 设备不支持蓝牙, Toast.LENGTH_SHORT).show() finish() return }3. 设备扫描与连接实战扫描BLE设备就像在人群中找熟人需要知道对方特征。我开发智能水杯项目时发现不同厂商的广播数据格式千奇百怪最后总结出这套稳定扫描方案构建扫描回调时要注意防坑private val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { result.device?.let { device - // 过滤无效设备名 if (!device.name.isNullOrEmpty()) { val rssi result.rssi // 信号强度 val scanRecord result.scanRecord?.bytes // 原始广播数据 handleDiscoveredDevice(device, rssi) } } } override fun onScanFailed(errorCode: Int) { when (errorCode) { SCAN_FAILED_ALREADY_STARTED - showToast(扫描已在进行中) SCAN_FAILED_APPLICATION_REGISTRATION_FAILED - showToast(应用注册失败) SCAN_FAILED_INTERNAL_ERROR - showToast(内部错误) SCAN_FAILED_FEATURE_UNSUPPORTED - showToast(不支持该功能) else - showToast(未知错误: $errorCode) } } }启动扫描的最佳实践fun startBleScan() { if (!bluetoothAdapter.isEnabled) { requestEnableBluetooth() return } val scanner bluetoothAdapter.bluetoothLeScanner val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟模式 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) .build() // 过滤指定服务UUID的设备可选 val filters listOf( ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID)) .build() ) scanner.startScan(filters, settings, scanCallback) scanTimer.postDelayed({ stopBleScan() }, SCAN_TIMEOUT) // 10秒后自动停止 }建立GATT连接时遇到过连接不稳定的问题后来发现是没处理MTU协商private val gattCallback object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED - { gatt.discoverServices() // 关键步骤 gatt.requestMtu(247) // 请求最大MTU } BluetoothProfile.STATE_DISCONNECTED - { gatt.close() reconnectDevice() } } } override onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { Log.d(BLE, MTU更新为: $mtu) // 典型值23/247 } } // 连接方法 fun connectDevice(device: BluetoothDevice) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { bluetoothGatt device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) } else { bluetoothGatt device.connectGatt(context, false, gattCallback) } }4. 数据通信与分包处理和智能锁厂商联调时发现20字节的MTU根本不够用——光加密指令就占了32字节。于是研究出这套数据分包方案写入大数据的可靠方法fun writeLargeData(data: ByteArray, characteristic: BluetoothGattCharacteristic) { val chunkSize bluetoothGatt?.getMtu()?.minus(3) ?: 20 // 默认20字节 data.toList().chunked(chunkSize).forEachIndexed { index, chunk - val packet when { index 0 - byteArrayOf(0x01) chunk.toByteArray() // 首包 index data.size/chunkSize - byteArrayOf(0x03) chunk.toByteArray() // 尾包 else - byteArrayOf(0x02) chunk.toByteArray() // 中间包 } characteristic.value packet bluetoothGatt?.writeCharacteristic(characteristic) Thread.sleep(20) // 防止写入过快 } }处理通知数据要考虑粘包问题private val buffer ByteArrayOutputStream() override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { val value characteristic.value when (value[0].toInt()) { 0x01 - { // 首包 buffer.reset() buffer.write(value, 1, value.size - 1) } 0x02 - { // 中间包 buffer.write(value, 1, value.size - 1) } 0x03 - { // 尾包 buffer.write(value, 1, value.size - 1) handleCompleteData(buffer.toByteArray()) } } }启用通知的正确姿势fun enableNotification(characteristic: BluetoothGattCharacteristic) { bluetoothGatt?.setCharacteristicNotification(characteristic, true) val descriptor characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID) descriptor.value BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE bluetoothGatt?.writeDescriptor(descriptor) }5. 典型问题排查指南连接失败是新手最常见的问题我整理了这个检查清单检查设备是否在广播用nRF Connect等工具验证确认没有其他App占着连接Android限制单设备单连接尝试不同的连接参数// Android 7.0 可以设置首选PHY if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { bluetoothGatt?.setPreferredPhy( BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_LE_2M_MASK, BluetoothDevice.PHY_OPTION_NO_PREFERRED ) }数据传输不稳定时可以尝试降低传输速率增加包间隔时间使用Write Without Response特性启用BLE Data Length ExtensionAndroid 8.0内存泄漏预防方案override fun onDestroy() { bluetoothGatt?.disconnect() bluetoothGatt?.close() bluetoothAdapter.bluetoothLeScanner?.stopScan(scanCallback) super.onDestroy() }最近给医院做体温贴项目时还遇到个信号干扰问题——当20个设备同时广播时手机几乎收不到数据。解决方案是错开设备广播间隔使用定向天线实现扫描窗口同步需要硬件配合BLE开发就像在钢丝上跳舞既要考虑协议规范又要适配各厂商的魔改实现。上周刚解决了个华为Mate 40的兼容性问题——它的后台扫描限制比其他手机严格得多。建议真机测试覆盖至少三款不同品牌设备别像我一样在客户现场才发现问题。