本文结合 BLE 开发核心原理与实战经验,深入剖析 Android 蓝牙读写的实现细节,涵盖特征值读写、命令队列实现、通知管理、多线程处理方案,助你打造简洁高效的实现方案。
一、特征读写
注意点:(参见BluetoothGatt.java源码)
读写操作是异步的。
一次只能执行一个异步操作。
1.1、特征值读写机制
BLE 的读写操作本质是异步的,需要通过回调接收结果。示例示例:
kotlin
// 特征值读取
fun readCharacteristic(characteristic: BluetoothGattCharacteristic) {
if (!checkReadPermission(characteristic)) return
enqueueCommand(BleCommand.Read(characteristic).apply {
onExecute = { gatt ->
if (!gatt.readCharacteristic(characteristic)) {
Log.e(TAG, "读取特征值失败")
completeCommand()
}
}
})
}
// 特征值写入(带重试机制)
fun writeCharacteristic(
characteristic: BluetoothGattCharacteristic,
data: ByteArray,
writeType: Int = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
) {
characteristic.apply {
value = data
this.writeType = writeType
}
enqueueCommand(BleCommand.Write(characteristic).apply {
maxRetries = 3
onExecute = { gatt ->
if (!gatt.writeCharacteristic(characteristic)) {
Log.e(TAG, "写入特征值失败")
completeCommand()
}
}
})
}
二、命令队列实现
使用协程队列管理异步操作(示例代码):
kotlin
private val commandQueue = LinkedList<BleCommand>()
private var isExecuting = AtomicBoolean(false)
sealed class BleCommand {
abstract fun execute(gatt: BluetoothGatt)
data class Read(val characteristic: BluetoothGattCharacteristic) : BleCommand()
data class Write(val characteristic: BluetoothGattCharacteristic) : BleCommand()
// 其他命令类型...
}
private fun enqueueCommand(command: BleCommand) {
synchronized(commandQueue) {
commandQueue.offer(command)
if (!isExecuting.get()) processNextCommand()
}
}
private fun processNextCommand() {
CoroutineScope(Dispatchers.IO).launch {
val cmd = synchronized(commandQueue) { commandQueue.poll() }
cmd?.let {
isExecuting.set(true)
it.execute(bluetoothGatt)
}
}
}
fun completeCommand() {
isExecuting.set(false)
processNextCommand()
}
三、通知管理和数量限制
3.1、通知管理
-
调用
setCharacteristicNotification
方法,将 1 或 2 作为 16位的 无符号整形,写入特征值的客户特性配置(CCC)描述符。CCC 描述符具有短的 UUID 2902 。 -
你需要知道为什么写入 1 或 2 ,这是因为底层有
Notifications
和Indications
。接收到的Indications
需要通过蓝牙栈来确认,而Notifications
不需要确认。通过使用Indications
,设备将有效地知道已经收到数据,并可以从本地存储中删除数据。 -
值 1 用于打开
Notifications
,值2用于Indications
。要关闭它们,请写 0 。 -
两种(
Notifications
和Indications
)情况并没有什么不同: 在这两种情况下,你将只会收到一个新的字节数组,而栈会在接收到指示时自动处理确认。
示例代码:
kotlin
// 启用通知
fun enableNotifications(characteristic: BluetoothGattCharacteristic) {
val cccdUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
characteristic.getDescriptor(cccdUuid)?.let { descriptor ->
bluetoothGatt.setCharacteristicNotification(characteristic, true)
val value = when {
characteristic.isIndicatable() -> BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
characteristic.isNotifiable() -> BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
else -> return
}
enqueueCommand(BleCommand.WriteDescriptor(descriptor, value))
}
}
// 处理通知回调
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val valueCopy = characteristic.value.copyOf() // 必须复制数据
CoroutineScope(Dispatchers.Default).launch {
processNotification(valueCopy)
}
}
3.2、通知数量限制
- 通知数量在不同版本中是不一致的,需要兼容。
示例代码:
kotlin
// 维护通知集合
private val activeNotifications = Collections.synchronizedSet(mutableSetOf<UUID>())
fun enableLimitedNotifications(characteristic: BluetoothGattCharacteristic) {
if (activeNotifications.size >= getMaxNotifications()) {
disableOldestNotification()
}
activeNotifications.add(characteristic.uuid)
enableNotifications(characteristic)
}
// 获取最大通知数量
private fun getMaxNotifications(): Int {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> 15
Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 -> 7
else -> 4
}
}
四、多线程处理方案
示例代码:
kotlin
// 使用协程管理线程
private val bleScope = CoroutineScope(Dispatchers.IO + Job())
// 特征值读取示例(带线程安全)
fun safeReadCharacteristic(characteristic: BluetoothGattCharacteristic) {
bleScope.launch {
withContext(Dispatchers.IO) {
if (!bluetoothGatt.readCharacteristic(characteristic)) {
Log.e(TAG, "读取操作失败")
return@withContext
}
// 使用挂起函数等待回调
val result = suspendCancellableCoroutine<ByteArray?> { cont ->
readCallback = { data ->
cont.resume(data)
}
}
result?.let { processData(it) }
}
}
}
// 使用独立 HandlerThread
private val bleHandler by lazy {
HandlerThread("BLE-Handler").apply { start() }.looper.let {
Handler(it)
}
}
fun postToBleThread(action: () -> Unit) {
bleHandler.post {
action()
Unit
}
}
五、最佳实践建议
-
数据拷贝原则:
- 在通知回调中立即复制字节数组
-
线程隔离:
- Binder 线程仅用于事件传递
- 使用独立线程处理业务逻辑
-
队列优化:
- 实现优先级队列处理紧急命令
- 添加超时机制(建议 30s)
-
资源管理:
- 及时关闭不需要的通知
- 在 onConnectionStateChange 中清理资源
-
错误处理:
- 区分 GATT 错误类型(status code)
- 实现自动重连机制
错误处理示例代码:
kotlin
// 错误处理示例
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
when (status) {
BluetoothGatt.GATT_SUCCESS -> {
Log.d(TAG, "写入成功")
}
BluetoothGatt.GATT_WRITE_NOT_PERMITTED -> {
Log.e(TAG, "无写入权限")
}
else -> {
Log.e(TAG, "写入失败,错误码:$status")
if (shouldRetry(status)) {
retryCurrentCommand()
}
}
}
}
建议结合具体业务场景封装基础操作,并通过单元测试验证关键流程。