ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android BLE开发实战:基于Kotlin协程与MVVM的现代蓝牙库设计

Android BLE开发实战:基于Kotlin协程与MVVM的现代蓝牙库设计 简介本资源是一套面向Android开发者与移动通信学习者的Kotlin蓝牙开发实战示例聚焦短距离无线通信场景解决蓝牙设备发现、配对、数据传输等核心功能的工程化实现问题适用于具备基础Android开发能力的学习者进阶实践。压缩包共326个文件总大小24.36MB涵盖62个Kotlin源文件含协程与扩展函数等现代语法实践、34个Java文件保障Java/Kotlin混合项目兼容性、133个XML布局与配置文件支撑UI与Manifest定义、17个AAR库含多个版本ppblutoothkit蓝牙SDK体现迭代适配过程以及14个SO本地库支撑底层蓝牙协议栈调用。已有423人学习下载资源结构清晰、模块完整提供从权限申请、扫描连接到数据收发的全链路代码参考并附带Gradle构建配置、Markdown说明文档及多分辨率PNG资源便于快速理解架构设计与移植集成。1. 项目缘起为什么我们需要一个Kotlin蓝牙库示例如果你是一名Android开发者最近在项目中需要集成蓝牙功能尤其是低功耗蓝牙BLE你大概率会和我有同样的感受官方文档的示例代码要么是Java的要么是过时的要么就是过于零散难以直接上手。更别提那些隐藏在BluetoothLeGatt示例项目中混杂着AsyncTask和Handler的老旧代码了。当你想用现代、简洁的Kotlin来重构时会发现网上能找到的要么是零碎的代码片段要么是封装得过于复杂、难以理解的第三方库。这就是我决定动手整理并开源一个“基于Kotlin语言的蓝牙库示例程序Android版设计源码”的直接原因。这个项目不是一个功能大而全的通用蓝牙框架它的核心定位非常明确一个清晰、现代、可直接复用的Kotlin BLE操作模板。它剥离了业务逻辑专注于展示在Android平台上如何使用Kotlin协程、Flow等现代语言特性优雅、安全地处理蓝牙扫描、连接、数据读写、通知监听等核心流程。在开始之前我们先明确一下这个示例程序的价值。它不仅仅是几行代码而是解决了一系列实际开发中的痛点架构清晰采用MVVM模式或更准确的一个简化的MVI思想分离了UI、业务逻辑和蓝牙底层操作便于理解和扩展。现代Kotlin实践全程使用Kotlin编写大量运用协程处理异步回调用StateFlow管理UI状态避免了回调地狱和内存泄漏。生命周期安全与Android的Lifecycle深度集成确保蓝牙操作在页面销毁时自动清理杜绝资源泄露。错误处理完备对蓝牙权限、位置服务、蓝牙开关状态、连接超时、服务发现失败等常见异常场景进行了封装和处理。可拔插设计核心的蓝牙管理器BluetoothManager接口化方便你替换为其他蓝牙库如RxAndroidBle或进行单元测试。这个项目适合所有正在或即将进行Android蓝牙开发的同行无论你是想快速搭建一个BLE功能原型还是想学习如何用Kotlin现代化地处理硬件交互它都能提供一个扎实的起点。接下来我将从环境搭建开始带你一步步拆解这个示例程序的设计与实现。2. 环境准备与项目结构概览在深入代码之前我们需要把环境搭建好。这个示例基于Android Studio进行开发对SDK版本和依赖库有明确要求。2.1 开发环境与依赖配置首先确保你的build.gradle (Module: app)文件中的配置如下。这里的关键是Kotlin协程和Lifecycle相关库它们是实现异步操作和生命周期感知的基石。android { compileSdk 34 defaultConfig { minSdk 21 // BLE需要API 18但为了更好的权限模型和现代API建议21 targetSdk 34 ... } buildFeatures { viewBinding true // 或使用Compose这里以ViewBinding为例 } kotlinOptions { jvmTarget 1.8 } } dependencies { implementation androidx.core:core-ktx:1.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 implementation androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 implementation org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 // 测试依赖 testImplementation junit:junit:4.13.2 androidTestImplementation androidx.test.ext:junit:1.1.5 androidTestImplementation androidx.test.espresso:espresso-core:3.5.1 }注意这里没有引入任何第三方蓝牙库。我们直接使用Android官方的android.bluetooth包目的是为了让你透彻理解原生API的工作机制。在实际大型项目中你可能会选择RxAndroidBle等库来简化操作但掌握底层原理是有效使用和调试高级库的前提。2.2 项目模块与包结构设计一个清晰的项目结构是代码可维护性的第一道保障。本示例采用了按功能分层的包结构而非按类型如把所有Activity放一起。以下是核心的包目录com.example.blekotlindemo/ ├── ui/ │ ├── MainActivity.kt // 主界面负责UI展示和用户交互 │ └── DeviceListFragment.kt // 设备列表Fragment ├── viewmodel/ │ └── BleViewModel.kt // 持有和处理蓝牙相关状态与逻辑 ├── bluetooth/ │ ├── manager/ │ │ ├── IBluetoothManager.kt // 蓝牙管理器接口 │ │ └── BluetoothManagerImpl.kt // 蓝牙管理器具体实现核心 │ ├── model/ │ │ ├── BleDevice.kt // 蓝牙设备数据类 │ │ ├── ConnectionState.kt // 连接状态枚举类 │ │ └── GattAction.kt // GATT操作类型读、写、通知等 │ └── callback/ │ └── SimplifiedBluetoothGattCallback.kt // 简化的GATT回调封装 ├── utils/ │ ├── PermissionsHelper.kt // 权限请求工具 │ └── Extensions.kt // Kotlin扩展函数 └── di/ (可选) └── 依赖注入相关设置如使用Koin或Hilt这种结构的好处一目了然ui层只关心界面和用户输入viewmodel作为中间层将bluetooth层的复杂操作转换为UI可观察的简单状态bluetooth层是真正的引擎负责所有与系统蓝牙API的交互。model包定义了数据传输对象callback包处理系统回调的转换。当你需要替换蓝牙实现或修改UI时影响范围被严格控制在了单个模块内。3. 核心实现从权限到连接的完整链路一切就绪我们进入最核心的部分。蓝牙开发的第一步永远不是打开蓝牙而是处理权限。Android的权限模型在不断演进处理BLE需要格外小心。3.1 运行时权限与蓝牙开关检测从Android 12 (API 31) 开始蓝牙扫描需要BLUETOOTH_SCAN权限并且该权限可以是neverForLocation的这解决了长期以来BLE扫描必须请求精确定位权限的尴尬。我们的PermissionsHelper需要智能地处理不同API版本。object PermissionsHelper { // 定义所需的权限数组 RequiresApi(Build.VERSION_CODES.S) fun getBlePermissions(): ArrayString arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT ) SuppressLint(InlinedApi) fun getBlePermissionsLegacy(): ArrayString arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, // API 31 需要位置权限 Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN ) fun checkAndRequestBlePermissions(activity: FragmentActivity): Boolean { val permissions if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { getBlePermissions() } else { getBlePermissionsLegacy() } val deniedPermissions permissions.filter { ContextCompat.checkSelfPermission(activity, it) ! PackageManager.PERMISSION_GRANTED }.toTypedArray() return if (deniedPermissions.isNotEmpty()) { activity.requestPermissions(deniedPermissions, REQUEST_CODE_BLE_PERMISSIONS) false } else { true } } }在MainActivity中我们这样使用它class MainActivity : AppCompatActivity() { private lateinit var viewModel: BleViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... 初始化UI和ViewModel lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { // 监听权限检查结果 viewModel.permissionGranted.collect { granted - if (granted) { checkBluetoothAndStartScan() } else { showPermissionRationale() } } } } } private fun checkBluetoothAndStartScan() { val bluetoothAdapter: BluetoothAdapter? BluetoothAdapter.getDefaultAdapter() when { bluetoothAdapter null - { // 设备不支持蓝牙 showError(设备不支持蓝牙) } !bluetoothAdapter.isEnabled - { // 请求用户打开蓝牙 val enableBtIntent Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) } else - { // 一切就绪通知ViewModel开始扫描 viewModel.startScan() } } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Arrayout String, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_CODE_BLE_PERMISSIONS) { val allGranted grantResults.all { it PackageManager.PERMISSION_GRANTED } viewModel.onPermissionResult(allGranted) } } }这里的关键点在于我们将权限状态和蓝牙开关状态都通过ViewModel中的StateFlow来管理。UIActivity/Fragment只负责发起请求和展示结果状态变化的逻辑集中在ViewModel中这使得代码更易于测试也避免了在生命周期复杂的Activity中埋下状态管理的隐患。3.2 蓝牙扫描的现代化封装传统的蓝牙扫描需要注册一个BroadcastReceiver来接收BluetoothDevice.ACTION_FOUND广播对于BLE则使用BluetoothLeScanner.startScan(scanCallback)。这些API都是基于回调的在Kotlin协程时代我们可以将其封装成更易用的Flow。在BluetoothManagerImpl中我们实现扫描功能class BluetoothManagerImpl Inject constructor( private val context: Context, private val scope: CoroutineScope ) : IBluetoothManager { private val _scanResults MutableStateFlowListBleDevice(emptyList()) override val scanResults: StateFlowListBleDevice _scanResults.asStateFlow() private var bluetoothLeScanner: BluetoothLeScanner? null private var scanCallback: ScanCallback? null override fun startScan() { val bluetoothAdapter: BluetoothAdapter? BluetoothAdapter.getDefaultAdapter() bluetoothLeScanner bluetoothAdapter?.bluetoothLeScanner if (bluetoothLeScanner null) { _scanError.tryEmit(蓝牙适配器不可用) return } // 停止之前的扫描如果存在 stopScan() // 清空旧结果 _scanResults.value emptyList() // 配置扫描过滤器这里不过滤扫描所有设备 val filters listOfScanFilter() // 空列表表示不过滤 val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 低延迟模式发现设备快但耗电 .build() scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult?) { result?.device?.let { device - val bleDevice BleDevice( name device.name ?: Unknown, address device.address, rssi result.rssi ) // 更新扫描结果这里简单去重按地址 _scanResults.update { list - if (list.any { it.address bleDevice.address }) { list.map { if (it.address bleDevice.address) bleDevice else it } } else { list bleDevice } } } } override fun onScanFailed(errorCode: Int) { _scanError.tryEmit(扫描失败错误码: $errorCode) } } try { bluetoothLeScanner?.startScan(filters, settings, scanCallback) _isScanning.value true } catch (e: SecurityException) { _scanError.tryEmit(无蓝牙扫描权限: ${e.message}) } catch (e: IllegalStateException) { _scanError.tryEmit(蓝牙适配器状态异常: ${e.message}) } } override fun stopScan() { scanCallback?.let { callback - try { bluetoothLeScanner?.stopScan(callback) } catch (e: Exception) { Log.e(BluetoothManager, 停止扫描时出错, e) } } scanCallback null _isScanning.value false } }在ViewModel中我们暴露一个简单的状态给UIclass BleViewModel Inject constructor( private val bluetoothManager: IBluetoothManager ) : ViewModel() { // UI可以直接观察这些StateFlow val scanResults: StateFlowListBleDevice bluetoothManager.scanResults val isScanning: StateFlowBoolean bluetoothManager.isScanning val connectionState: StateFlowConnectionState bluetoothManager.connectionState fun startScan() { viewModelScope.launch { bluetoothManager.startScan() } } fun stopScan() { viewModelScope.launch { bluetoothManager.stopScan() } } }这样在Fragment中我们只需要监听scanResults这个StateFlow列表就会自动更新。这种响应式编程模式极大地简化了UI逻辑。3.3 设备连接、服务发现与数据通信扫描到设备后下一步就是连接。这是BLE开发中最复杂的一环涉及BluetoothGatt的一系列异步回调。我们的目标是将其封装成顺序执行的协程挂起函数。首先在IBluetoothManager接口中定义连接函数interface IBluetoothManager { suspend fun connect(deviceAddress: String): ResultUnit fun disconnect() suspend fun writeCharacteristic(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray): ResultUnit suspend fun readCharacteristic(serviceUuid: UUID, characteristicUuid: UUID): ResultByteArray fun enableNotification(serviceUuid: UUID, characteristicUuid: UUID, enable: Boolean) // ... 其他状态Flow }在BluetoothManagerImpl中实现connect函数。这里的关键是使用suspendCancellableCoroutine将回调转换为协程override suspend fun connect(deviceAddress: String): ResultUnit suspendCancellableCoroutine { continuation - val bluetoothAdapter BluetoothAdapter.getDefaultAdapter() val device bluetoothAdapter?.getRemoteDevice(deviceAddress) if (device null) { continuation.resume(Result.failure(IllegalArgumentException(设备地址无效或未找到设备))) returnsuspendCancellableCoroutine } // 先断开之前的连接如果有 disconnectGatt() _connectionState.value ConnectionState.CONNECTING currentDeviceAddress deviceAddress // 注意这里使用 autoConnect false 以快速连接实际可根据场景调整 val gatt if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE) } else { device.connectGatt(context, false, gattCallback) } this.bluetoothGatt gatt // 设置一个连接超时 scope.launch { delay(CONNECTION_TIMEOUT_MS) if (_connectionState.value ConnectionState.CONNECTING) { disconnectGatt() continuation.resume(Result.failure(TimeoutException(连接超时))) } } // 在GattCallback中处理连接结果 gattCallback.onConnected { gatt - _connectionState.value ConnectionState.CONNECTED continuation.resume(Result.success(Unit)) } gattCallback.onConnectionFailed { exception - _connectionState.value ConnectionState.DISCONNECTED continuation.resume(Result.failure(exception ?: Exception(连接失败))) } }这里的gattCallback是我们封装的SimplifiedBluetoothGattCallback它内部处理了onConnectionStateChange、onServicesDiscovered、onCharacteristicRead/Write、onCharacteristicChanged等所有回调并将它们转换为更易处理的事件或挂起函数的续体continuation。服务发现通常在连接成功后自动或手动触发。在我们的设计中连接成功后会自动开始发现服务// 在 SimplifiedBluetoothGattCallback 的 onConnectionStateChange 中 override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTED - { // 连接成功开始发现服务 gatt.discoverServices() onConnected?.invoke(gatt) } BluetoothProfile.STATE_DISCONNECTED - { onDisconnected?.invoke() gatt.close() } } }发现服务成功后我们就可以进行读写操作了。以写特征值为例override suspend fun writeCharacteristic(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray): ResultUnit suspendCancellableCoroutine { continuation - val gatt bluetoothGatt if (gatt null || _connectionState.value ! ConnectionState.CONNECTED) { continuation.resume(Result.failure(IllegalStateException(未连接或Gatt对象为空))) returnsuspendCancellableCoroutine } val service gatt.getService(serviceUuid) val characteristic service?.getCharacteristic(characteristicUuid) if (characteristic null) { continuation.resume(Result.failure(IllegalArgumentException(未找到指定的服务或特征))) returnsuspendCancellableCoroutine } // 设置特征值并指定写类型 characteristic.value data // 根据特征属性决定写入类型 val writeType when { characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE 0 - { BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE } characteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE 0 - { BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT } else - { continuation.resume(Result.failure(UnsupportedOperationException(该特征不支持写入))) returnsuspendCancellableCoroutine } } characteristic.writeType writeType // 将回调与本次挂起关联起来 gattCallback.pendingWriteContinuation continuation if (!gatt.writeCharacteristic(characteristic)) { gattCallback.pendingWriteContinuation null continuation.resume(Result.failure(IOException(写入请求发送失败))) } }在SimplifiedBluetoothGattCallback的onCharacteristicWrite回调中我们需要取出对应的continuation并恢复它override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { val cont pendingWriteContinuation pendingWriteContinuation null if (status BluetoothGatt.GATT_SUCCESS) { cont?.resume(Result.success(Unit)) } else { cont?.resume(Result.failure(IOException(写入失败状态码: $status))) } }通过这种方式我们将所有异步、基于回调的蓝牙API封装成了线性的、可读性极强的协程挂起函数。在ViewModel或业务层你可以像调用普通函数一样使用它们viewModelScope.launch { when (val result bluetoothManager.connect(deviceAddress)) { is Result.Success - { // 连接成功可以开始读写操作 val writeResult bluetoothManager.writeCharacteristic( serviceUuid SERVICE_UUID_HEART_RATE, characteristicUuid CHAR_UUID_HEART_RATE_MEASUREMENT, data byteArrayOf(0x01) // 例如使能通知 ) if (writeResult.isSuccess) { // 写入成功 } } is Result.Failure - { // 处理连接失败 showError(result.exception.message) } } }4. 状态管理与UI联动的实战技巧将底层蓝牙操作封装好后如何优雅地在UI上反映状态变化是提升用户体验的关键。我们使用StateFlow和ViewModel来构建响应式UI。4.1 使用Sealed Class定义清晰的UI状态对于连接状态一个简单的枚举可能不够。我们使用密封类Sealed Class来定义所有可能的UI状态这比使用多个独立的LiveData或Flow更清晰也便于Compose或DataBinding使用。// 在 BleViewModel 或一个独立的状态类中 sealed class BleUiState { object Idle : BleUiState() // 初始空闲状态 object Scanning : BleUiState() // 扫描中 data class ScanResults(val devices: ListBleDevice) : BleUiState() // 扫描结果 object Connecting : BleUiState() // 连接中 data class Connected(val deviceName: String) : BleUiState() // 已连接 data class DataReceived(val data: ByteArray) : BleUiState() // 收到数据 data class Error(val message: String) : BleUiState() // 错误状态 object Disconnected : BleUiState() // 已断开 } // 在ViewModel中合并多个状态流 class BleViewModel Inject constructor( private val bluetoothManager: IBluetoothManager ) : ViewModel() { private val _uiState MutableStateFlowBleUiState(BleUiState.Idle) val uiState: StateFlowBleUiState _uiState.asStateFlow() init { viewModelScope.launch { // 合并扫描状态和连接状态驱动UI combine( bluetoothManager.isScanning, bluetoothManager.connectionState, bluetoothManager.scanResults, bluetoothManager.receivedData ) { isScanning, connState, devices, data - when { isScanning - BleUiState.Scanning connState ConnectionState.CONNECTED - BleUiState.Connected(bluetoothManager.connectedDeviceName ?: Unknown) connState ConnectionState.CONNECTING - BleUiState.Connecting connState ConnectionState.DISCONNECTED devices.isEmpty() - BleUiState.Idle connState ConnectionState.DISCONNECTED - BleUiState.ScanResults(devices) data ! null - BleUiState.DataReceived(data) else - BleUiState.Idle } }.collect { newState - _uiState.value newState } } // 单独收集错误流 viewModelScope.launch { bluetoothManager.errorMessages.collect { errorMsg - if (errorMsg.isNotBlank()) { _uiState.value BleUiState.Error(errorMsg) } } } } }在UI层Activity/Fragment观察这个统一的uiState即可// 在Fragment中 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state - when (state) { is BleUiState.Scanning - { binding.progressBar.visibility View.VISIBLE binding.scanButton.text 停止扫描 } is BleUiState.ScanResults - { binding.progressBar.visibility View.GONE adapter.submitList(state.devices) } is BleUiState.Connecting - { showToast(正在连接...) } is BleUiState.Connected - { showToast(已连接到 ${state.deviceName}) // 更新UI显示数据交互界面 } is BleUiState.Error - { showErrorDialog(state.message) } // ... 处理其他状态 } } } }4.2 处理屏幕旋转与进程死亡蓝牙连接是长时操作且持有系统资源BluetoothGatt。必须妥善处理配置变更如屏幕旋转和进程死亡。1. 使用ViewModel保存关键状态ViewModel在配置变更时不会销毁因此我们将设备地址、连接状态等保存在ViewModel中。旋转屏幕后ViewModel可以尝试重新连接。2. 在onCleared中释放资源当ViewModel不再需要时如Activity被finish必须断开蓝牙连接并释放资源。override fun onCleared() { super.onCleared() viewModelScope.launch { bluetoothManager.disconnect() bluetoothManager.stopScan() } }3. 处理进程死亡可选高级场景如果你的应用需要后台保持连接可以考虑使用Foreground Service并将关键状态如设备地址保存到SharedPreferences或DataStore中。当应用从进程死亡中恢复时ViewModel会重新创建此时可以从持久化存储中读取设备地址并尝试重新连接。本示例程序聚焦于前台交互暂不涉及此复杂场景。4.3 通知Notification的启用与数据监听对于需要设备主动上报数据的特征如心率测量需要启用通知Notification或指示Indication。fun enableNotification(serviceUuid: UUID, characteristicUuid: UUID, enable: Boolean) { val gatt bluetoothGatt ?: return val service gatt.getService(serviceUuid) ?: return val characteristic service.getCharacteristic(characteristicUuid) ?: return // 1. 先设置客户端特征配置描述符CCCD val descriptor characteristic.getDescriptor(CCC_DESCRIPTOR_UUID) // UUID: 0x2902 descriptor?.value if (enable) { BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE } else { BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE } // 2. 写入描述符 gatt.writeDescriptor(descriptor) // 3. 如果写入成功在onDescriptorWrite回调中再设置特征值的通知 gattCallback.onDescriptorWriteSucceeded { desc - if (desc.uuid CCC_DESCRIPTOR_UUID) { gatt.setCharacteristicNotification(characteristic, enable) } } }启用后设备发送的数据会触发SimplifiedBluetoothGattCallback的onCharacteristicChanged回调我们在这里将数据通过Flow发送出去// 在 SimplifiedBluetoothGattCallback 中 override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { val data characteristic.value _receivedData.tryEmit(data) }在ViewModel中收集这个receivedDataFlow并合并到uiState中UI就能实时更新了。5. 避坑指南与性能优化纸上得来终觉浅绝知此事要躬行。下面分享几个我在实际开发中踩过的坑和总结的优化点这些在官方文档里往往不会细说。5.1 连接失败与“133”错误码这是BLE开发中最常见的错误之一。当你调用connectGatt后onConnectionStateChange回调中的status参数可能会返回133或其他非0值紧接着状态变为STATE_DISCONNECTED。可能的原因和解决方案系统层面限制部分手机厂商特别是国内定制ROM对后台扫描和连接有严格限制。确保你的应用在前台运行并且用户给予了所有必要权限包括后台位置权限如果需要。设备端拒绝有些BLE设备有连接间隔、安全要求等限制。检查设备文档确认你的手机兼容性。Gatt对象未及时关闭同一个设备在断开连接后必须调用gatt.close()释放资源否则再次连接可能会失败。确保你的disconnect逻辑里包含了close()。连接超时像我们上面实现的添加一个连接超时机制如30秒是非常必要的。超时后主动断开并清理给用户明确的反馈。重试策略对于偶发的连接失败可以实现一个简单的指数退避重试机制但不要无限重试通常2-3次后就应该提示用户检查设备和环境。5.2 扫描耗电与后台限制持续扫描是耗电大户。我们的示例中使用了SCAN_MODE_LOW_LATENCY这在前台快速发现设备时是合适的。但在实际应用中需要考虑更多场景前台扫描使用SCAN_MODE_LOW_LATENCY或SCAN_MODE_BALANCED。后台扫描如果应用需要在后台持续扫描如Beacon应用必须使用SCAN_MODE_LOW_POWER并且从Android 8.0开始后台扫描有严格的限制时间窗口、发现次数限制。通常需要结合AlarmManager或WorkManager进行周期扫描。扫描过滤器使用ScanFilter可以大幅减少不必要的回调节省电量。例如只扫描特定服务UUID或设备名称的设备。val filter ScanFilter.Builder() .setServiceUuid(ParcelUuid(SERVICE_UUID_HEART_RATE)) .build() val filters listOf(filter)5.3 读写操作超时与队列管理Android的BLE栈内部有一个操作队列。如果你在前一个写操作的回调onCharacteristicWrite收到之前又发起了下一个写操作可能会导致第二个操作失败或行为异常。解决方案实现一个简单的操作队列。class BluetoothManagerImpl { private val operationQueue ChannelGattOperation(capacity Channel.UNLIMITED) private val operationScope CoroutineScope(Dispatchers.IO SupervisorJob()) init { operationScope.launch { for (op in operationQueue) { try { when (op) { is GattOperation.Write - { performWrite(op.serviceUuid, op.charUuid, op.data) } is GattOperation.Read - { performRead(op.serviceUuid, op.charUuid) } } } catch (e: Exception) { // 处理单个操作失败不影响队列继续执行 _errorMessages.tryEmit(操作失败: ${e.message}) } } } } suspend fun writeCharacteristicQueued(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray) { operationQueue.send(GattOperation.Write(serviceUuid, characteristicUuid, data)) } private suspend fun performWrite(serviceUuid: UUID, characteristicUuid: UUID, data: ByteArray) { // 这里使用我们之前实现的挂起函数 writeCharacteristic // 但确保它是顺序执行的 writeCharacteristic(serviceUuid, characteristicUuid, data).fold( onSuccess { /* 成功处理 */ }, onFailure { throw it } ) } } sealed class GattOperation { data class Write(val serviceUuid: UUID, val charUuid: UUID, val data: ByteArray) : GattOperation() data class Read(val serviceUuid: UUID, val charUuid: UUID) : GattOperation() }这样所有读写操作都会按顺序执行避免了并发问题。对于需要高吞吐量的场景你可能需要更复杂的队列优先级管理但对于大多数应用一个FIFO队列已经足够。5.4 内存泄漏预防蓝牙相关的回调持有Context或Activity引用是内存泄漏的常见根源。在BluetoothManager中持有Application Context在初始化BluetoothManagerImpl时传入Application Context通过依赖注入或context.applicationContext而不是Activity Context。及时取消协程所有在viewModelScope或自定义scope中启动的协程都会在ViewModel的onCleared或scope取消时自动取消。确保你的蓝牙操作如连接超时是可取消的使用suspendCancellableCoroutine。解除回调引用在BluetoothManager的disconnect和cleanup方法中不仅要将bluetoothGatt置为null还要将gattCallback内部对continuation等临时引用也置为null。这个基于Kotlin的蓝牙库示例程序从权限处理、扫描、连接到数据读写完整地展示了一套现代化、健壮且易于理解的Android BLE开发实践。它没有追求大而全的功能而是力求在每一个环节都做到清晰和可靠。你可以直接将它作为新项目的基础模块也可以从中抽取思想来改造现有的蓝牙代码。最重要的是希望它能帮助你避开那些我曾经踩过的坑更顺畅地开发出稳定可靠的蓝牙应用。本文还有配套的精品资源点击获取
返回列表