上一篇聊了依赖管理,这篇进入一个特定领域——蓝牙低功耗(BLE)。智能手表、健康设备、IoT 传感器都走 BLE 协议,但 Android 的 BLE API 出了名的难用:回调嵌套、状态机复杂、不同手机行为不一致。这篇梳理 BLE 开发的核心流程和常见坑。
BLE 基本概念
BLE 通信涉及几个概念:
Central(中心设备):你的手机,发起连接
Peripheral(外围设备):手环、传感器等,广播等待连接
Service(服务):一组相关功能的集合,用 UUID 标识
Characteristic(特征值):数据的最小单位,可读/可写/可通知
Descriptor(描述符):特征的配置信息
数据流向:手机连接设备 → 发现 Service → 获取 Characteristic → 读/写数据或监听通知。
权限配置
<!-- Android 12 以下 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 扫描需要 -->
<!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
Android 12 把蓝牙权限拆分了,SCAN 和 CONNECT 分开申请。建议同时声明新旧权限,用 maxSdkVersion 兼容:
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
扫描设备
val bluetoothAdapter = (context.getSystemService(Context.BLUETOOTH_SERVICE)
as BluetoothManager).adapter
// Android 5.0+
val scanner = bluetoothAdapter.bluetoothLeScanner
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val rssi = result.rssi // 信号强度
Log.d("BLE", "Found: ${device.address}, RSSI: $rssi")
}
}
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
scanner.startScan(emptyList(), settings, scanCallback)
// 5-10 秒后停止
handler.postDelayed({
scanner.stopScan(scanCallback)
}, 10000)
扫描的坑:
必须加超时。不主动停止扫描会持续耗电,且某些 ROM 会限制扫描时间。
SCAN_MODE_LOW_LATENCY 功耗大,适合首次连接。持续扫描用
BALANCED或LOW_POWER。过滤规则。按 UUID 或设备名过滤可以减少回调频率,但不同手机对过滤的支持有差异。建议不过滤,在回调中手动筛。
Android 7+ 重复扫描限制。30 秒内扫描超过 5 次会被系统限流,回调延迟。
连接设备
device.connectGatt(context, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
gatt.discoverServices() // 连接后必须发现服务
}
BluetoothProfile.STATE_DISCONNECTED -> {
gatt.close() // 断开后必须 close
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val service = gatt.getService(UUID.fromString(SERVICE_UUID))
val characteristic = service?.getCharacteristic(UUID.fromString(CHAR_UUID))
// 可以开始读/写/订阅
}
})
连接的坑:
connectGatt 的 autoConnect 参数。传 true 会自动重连,但延迟可能几秒到几分钟。传 false 连接快但不自动重连。建议手动管理重连。
onServicesDiscovered 可能失败。status != GATT_SUCCESS 时要重试或断开重连。
GATT 133 错误。最常见也最让人头疼,通常是连接超时、设备不在线、或协议不匹配。重试 2-3 次仍失败就放弃。
不能同时连接多个设备操作同一特征值。串行操作,等上一个回调回来再发下一个。
读写数据
// 读
gatt.readCharacteristic(characteristic)
override fun onCharacteristicRead(gatt, characteristic, value, status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val data = characteristic.value // Android 12 以下
// 或 value 参数(Android 13+)
}
}
// 写
characteristic.value = data
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
gatt.writeCharacteristic(characteristic)
override fun onCharacteristicWrite(gatt, characteristic, status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 写入成功,可以发下一包
}
}
读写的关键:串行化。BLE 不支持并发操作,必须等上一个操作的回调返回后才能发下一个。用一个队列管理:
class BleOperationQueue {
private val queue = ArrayDeque<BleOperation>()
private var pending: BleOperation? = null
fun enqueue(op: BleOperation) {
queue.addLast(op)
if (pending == null) executeNext()
}
fun notifyComplete() {
pending = null
executeNext()
}
private fun executeNext() {
pending = queue.removeFirstOrNull()
pending?.execute()
}
}
通知(Notify)
持续接收数据用 Notify:
// 开启通知
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(
UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG)
)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
override fun onCharacteristicChanged(gatt, characteristic, value) {
// 收到设备推送的数据
val data = value // Android 13+
}
Notify 是 BLE 最常用的数据接收方式。写 Descriptor 开启通知是必须的,否则收不到数据。
Android 13 的 API 变更
Android 13(API 33)对 BLE API 做了较大调整,readCharacteristic、writeCharacteristic、writeDescriptor 等方法签名变了,直接传入 value 而不是设到对象上:
// Android 13+
gatt.writeCharacteristic(characteristic, data, WRITE_TYPE_DEFAULT)
// 旧 API
characteristic.value = data
gatt.writeCharacteristic(characteristic)
兼容方案:用 @RequiresApi 注解或封装一个 BleHelper 做版本判断。
小结
Android BLE 开发的核心是理解状态机 + 串行化操作。回调嵌套虽然丑但没好办法,用协程或队列封装可以改善。连接状态管理、GATT 133 错误处理、多设备并发是三大难点。建议在正式开发前先用 nRF Connect 之类的工具验证设备协议,再写代码。
下一篇聊聊 CameraX:从 Camera2 到 CameraX 的迁移。