You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android平台基于BLE API的大文件传输优化方案问询

BLE大字节数组发送的优化方案与实现调整

针对你实现的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)
    }
}

代码关键优化点说明:

  1. 移除了固定发送延迟:所有写入操作完成后,通过onCharacteristicWrite回调触发sendNext(),完全适配BLE异步特性
  2. 优化了prepareSending方法:提前计算所有分块的数量和容量,减少运行时计算开销,用minOf替代手动判断,代码更简洁
  3. 补充了扩展方法:把校验和计算封装成ByteArray的扩展函数,代码更整洁
  4. 简化了初始化信息的发送:取消了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 03:38:36