Android平台基于BLE API的大文件传输优化方案问询
针对你实现的BLE大数组发送类,我来分享几个更优的实现思路,同时解答你关于写入延迟的问题:
一、更优实现方案建议
1. 优先使用指示(Indication)替代通知(Notification)
如果你的外设支持,把数据发送特征值的通知改成指示(设置描述符为BluetoothGattDescriptor.ENABLE_INDICATION_VALUE)。指示是带ACK的BLE通信机制,系统会自动确保每一次写入都被外设确认,比你自己做校验和ACK的方式更原生可靠,能减少手动处理重传的复杂度。
2. 强化状态机管理
把发送流程拆成明确的状态节点:
STATE_WAIT_NOTIFY_ENABLE:等待通知/指示启用完成STATE_WAIT_DEVICE_INIT:等待外设返回初始化确认(即你说的“等待外设通知可发送第0块”)STATE_SENDING_CHUNK:正在发送当前大分块的小数据块STATE_WAIT_CHUNK_ACK:等待外设的校验和确认STATE_FINISHED/STATE_FAILED:发送完成/失败
状态机可以用枚举类实现,让代码逻辑更清晰,避免currentIndex这类变量的混乱。
3. 优化分块与校验逻辑
你的分块策略(1000字节大分块,再拆成2字节编号+18字节数据的小分块)是合理的,刚好适配BLE 20字节的有效负载(MTU=23时,ATT头占3字节)。另外可以:
- 提前计算所有大分块的校验和,而不是在
prepareSending中动态计算(你的代码已经在做了,这点很好) - 把小分块的编号改成1字节(如果大分块的小分块总数不超过255的话),这样可以多塞1字节数据,减少总发送次数
4. 精细化错误处理
你现有的FailureCheck是针对外设未响应的超时重试,还可以补充:
- 在
onCharacteristicWrite回调中捕获写入失败的情况(比如status != BluetoothGatt.GATT_SUCCESS),直接重试当前小分块 - 记录重试次数,超过阈值再触发整体失败,避免无限重试
二、关于20ms写入延迟的解决方案
完全可以避免! 你改用等待onCharacteristicWrite回调的方向是完全正确的——固定延迟是早期的妥协方案,而回调驱动才是适配BLE异步特性的最优解。
具体做法是:在你的BluetoothLeService的onCharacteristicWrite方法中,判断写入的特征值是数据发送特征值,并且写入成功后,调用bluetoothLEDataSender.sendNext()来触发下一块的发送。这样系统会在每一次写入完成后才发送下一块,既不会浪费时间,也不会因为写入过快导致系统丢弃请求。
三、优化后的代码实现
class BluetoothLEDataSender( val characteristicForSending: BluetoothGattCharacteristic, val characteristicForNotifyDataSend: BluetoothGattCharacteristic, private val config: BluetoothLESenderConfiguration = BluetoothLESenderConfiguration(), val bluetoothLeService: WeakReference<BluetoothLeService> ) : HandlerThread("BluetoothLEDataSender") { data class BluetoothLESenderConfiguration( val retryForFailureInSeconds: Long = 3, val chunkSize: Int = 1000 ) private val toaster by lazy { Toast.makeText(bluetoothLeService.get()!!,"",Toast.LENGTH_SHORT) } companion object { val ACTION_DATA_SEND_FINISHED = "somatix.com.bleplays.ACTION_DATA_SEND_FINISHED" val ACTION_DATA_SEND_FAILED = "somatix.com.bleplays.ACTION_DATA_SEND_FAILED" private const val DESCRIPTOR_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" } lateinit var dataToSend: List<BlocksQueue> val messageHandler by lazy { SenderHandler() } var currentIndex = 0 private var currentBlock = 0 private val listOfCheckSums = mutableListOf<Int>() public fun notifyDataState(receivedChecksum: String) { val msg = Message() msg.arg1 = receivedChecksum.toInt() messageHandler.sendMessage(msg) } inner class BlocksQueue(val initialCapacity: Int) : ArrayBlockingQueue<ByteArray>(initialCapacity) inner class BlockSendingTask : Runnable { override fun run() { executeOnUiThread({ toaster.setText("Executing block: $currentIndex") toaster.show() }) sendNext() } } public fun sendMessage(messageByteArray: ByteArray) { start() dataToSend = prepareSending(messageByteArray) // 启用通知/指示 bluetoothLeService.get()?.setEnableNotification(characteristicForSending, true) val descriptor = characteristicForSending.getDescriptor(DESCRIPTOR_CONFIG_UUID) descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE bluetoothLeService.get()?.writeDescriptor(descriptor) // 发送初始化信息 characteristicForNotifyDataSend.value = "${messageByteArray.size}:${config.chunkSize}:${dataToSend.size}".toByteArray() toaster.setText(String(characteristicForNotifyDataSend.value)) toaster.show() // 取消固定延迟,直接写入,等待回调触发后续流程 bluetoothLeService.get()?.writeCharacteristic(characteristicForNotifyDataSend) } private fun prepareSending(messageByteArray: ByteArray): ArrayList<BlocksQueue> { with(config) { val chunksNumber = if (messageByteArray.size % chunkSize == 0) { messageByteArray.size / chunkSize } else { messageByteArray.size / chunkSize + 1 } val chunksArray = ArrayList<BlocksQueue>() (0 until chunksNumber).mapTo(chunksArray) { chunkIdx -> val start = chunkIdx * chunkSize val end = minOf(start + chunkSize, messageByteArray.size) val sliceArray = messageByteArray.sliceArray(start until end) // 提前计算校验和 val checksum = sliceArray.checkSum() listOfCheckSums.add(checksum) // 提前计算小分块数量(18字节/块)+1个校验和块 val smallBlockCount = if (sliceArray.size % 18 == 0) { sliceArray.size / 18 } else { sliceArray.size / 18 + 1 } val queue = BlocksQueue(smallBlockCount + 1) // 填充小分块 for (i in 0 until smallBlockCount) { val blockStart = i * 18 val blockEnd = minOf(blockStart + 18, sliceArray.size) queue.add(sliceArray.sliceArray(blockStart until blockEnd)) } // 添加校验和块 queue.add(checksum.toByteArray()) queue } return chunksArray } } fun sendNext() { val currentChunk = dataToSend.get(currentIndex) val peek = currentChunk.poll() ?: run { Log.i(TAG, "Finished chunk $currentIndex") currentBlock = 0 return } characteristicForSending.value = if (currentChunk.remainingCapacity() > 0) { // 小分块:块编号(2字节)+18字节数据 val indexStr = if (currentBlock > 9) "$currentBlock" else "0$currentBlock" indexStr.toByteArray() + peek } else { // 最后一块是校验和 peek } // 写入特征值,等待onCharacteristicWrite回调触发下一块 bluetoothLeService.get()?.writeCharacteristic(characteristicForSending) currentBlock++ } private val TAG = "BluetoothLeService" @SuppressLint("HandlerLeak") inner class SenderHandler : Handler(looper) { private var failureCheck: FailureCheck? = null override fun handleMessage(msg: Message) { super.handleMessage(msg) currentIndex = msg.arg1 if (currentIndex < dataToSend.size) { failureCheck?.let { removeCallbacks(it) } failureCheck = FailureCheck(currentIndex) post(BlockSendingTask()) postDelayed( failureCheck, TimeUnit.MILLISECONDS.convert(config.retryForFailureInSeconds, TimeUnit.SECONDS) ) } else { failureCheck?.let { removeCallbacks(it) } val intent = Intent(ACTION_DATA_SEND_FINISHED) bluetoothLeService.get()?.sendBroadcast(intent) } } private inner class FailureCheck(val index: Int) : Runnable { override fun run() { if (index == currentIndex) { val intent = Intent(ACTION_DATA_SEND_FAILED) bluetoothLeService.get()?.sendBroadcast(intent) } } } } // 扩展方法:计算字节数组校验和 private fun ByteArray.checkSum(): Int { return this.sumOf { it.toInt() and 0xFF } } // 工具方法:切换到UI线程 private fun executeOnUiThread(action: () -> Unit) { bluetoothLeService.get()?.runOnUiThread(action) } }
代码关键优化点说明:
- 移除了固定发送延迟:所有写入操作完成后,通过
onCharacteristicWrite回调触发sendNext(),完全适配BLE异步特性 - 优化了prepareSending方法:提前计算所有分块的数量和容量,减少运行时计算开销,用
minOf替代手动判断,代码更简洁 - 补充了扩展方法:把校验和计算封装成ByteArray的扩展函数,代码更整洁
- 简化了初始化信息的发送:取消了
postDelayed,直接写入特征值,等待回调
最后注意事项
别忘了在你的BluetoothLeService中实现onCharacteristicWrite方法,添加以下逻辑:
override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) { super.onCharacteristicWrite(gatt, characteristic, status) if (status == BluetoothGatt.GATT_SUCCESS) { // 判断是数据发送特征值,触发下一块发送 if (characteristic?.uuid == bluetoothLEDataSender.characteristicForSending.uuid) { bluetoothLEDataSender.sendNext() } } else { // 写入失败,处理重试逻辑 Log.e(TAG, "Characteristic write failed, status: $status") // 可以在这里调用sendNext()重试当前块,或者加入重试次数限制 } }
内容的提问来源于stack exchange,提问作者libinm

