Android_BLE开发——读写

本文结合 BLE 开发核心原理与实战经验,深入剖析 Android 蓝牙读写的实现细节,涵盖特征值读写、命令队列实现、通知管理、多线程处理方案,助你打造简洁高效的实现方案。

一、特征读写

注意点:(参见BluetoothGatt.java源码)

  1. 读写操作是异步的。
  2. 一次只能执行一个异步操作。

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方法,将 12 作为 16位的 无符号整形,写入特征值的客户特性配置(CCC)描述符。CCC 描述符具有短的 UUID 2902

  • 你需要知道为什么写入 12 ,这是因为底层有 NotificationsIndications 。接收到的 Indications 需要通过蓝牙栈来确认,而 Notifications 不需要确认。通过使用 Indications,设备将有效地知道已经收到数据,并可以从本地存储中删除数据。

  • 1 用于打开 Notifications ,值2用于 Indications。要关闭它们,请写 0

  • 两种(NotificationsIndications)情况并没有什么不同: 在这两种情况下,你将只会收到一个新的字节数组,而栈会在接收到指示时自动处理确认。

示例代码:

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()
            }
        }
    }
}

建议结合具体业务场景封装基础操作,并通过单元测试验证关键流程。

更多分享

  1. Android_BLE开发------初识BLE
  2. Android_BLE开发------扫描
  3. Android_BLE开发------连接
  4. Android_BLE开发------绑定
  5. Android_BLE开发------优化
相关推荐
selt79119 小时前
Redisson之RedissonLock源码完全解析
android·java·javascript
Yao_YongChao20 小时前
Android MVI处理副作用(Side Effect)
android·mvi·mvi副作用
非凡ghost21 小时前
JRiver Media Center(媒体管理软件)
android·学习·智能手机·媒体·软件需求
席卷全城21 小时前
Android 推箱子实现(引流文章)
android
齊家治國平天下21 小时前
Android 14 系统中 Tombstone 深度分析与解决指南
android·crash·系统服务·tombstone·android 14
maycho1231 天前
MATLAB环境下基于双向长短时记忆网络的时间序列预测探索
android
思成不止于此1 天前
【MySQL 零基础入门】MySQL 函数精讲(二):日期函数与流程控制函数篇
android·数据库·笔记·sql·学习·mysql
brave_zhao1 天前
达梦数据库(DM8)支持全文索引功能,但并不直接兼容 MySQL 的 FULLTEXT 索引语法
android·adb
sheji34161 天前
【开题答辩全过程】以 基于Android的网上订餐系统为例,包含答辩的问题和答案
android
easyboot1 天前
C#使用SqlSugar操作mysql数据库
android·sqlsugar