1. 项目背景与核心价值
在物联网和智能硬件快速发展的今天,低功耗蓝牙(Bluetooth Low Energy,简称BLE)已经成为连接智能设备的首选方案。相比传统蓝牙,BLE在保持足够通信距离的同时,功耗仅为传统蓝牙的1/10到1/100。这使得它非常适合可穿戴设备、健康监测仪、智能家居等需要长时间运行的场景。
Kotlin作为Android开发的官方推荐语言,其简洁的语法和强大的功能让BLE开发变得更加高效。特别是在处理异步操作和回调时,Kotlin的协程可以显著简化代码结构,避免"回调地狱"的问题。我在多个商业项目中采用Kotlin实现BLE连接,发现开发效率比Java实现提升了约30%,同时代码可维护性也大幅提高。
2. 开发环境准备
2.1 基础环境配置
要开发BLE应用,首先需要确保开发环境正确配置。Android Studio是最佳选择,建议使用最新稳定版本。在项目的build.gradle文件中,需要添加以下Kotlin依赖:
kotlin复制plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion 21 // BLE需要Android 5.0以上
targetSdkVersion 33
}
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.core:core-ktx:1.9.0'
}
注意:minSdkVersion必须≥21,因为Android对BLE的完整支持是从5.0(API 21)开始的。虽然4.3(API 18)就引入了BLE,但功能不完整。
2.2 权限声明
BLE操作需要多个权限,在AndroidManifest.xml中添加:
xml复制<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
对于Android 12及以上版本,还需要在运行时请求这些权限。这里有个坑:即使你的应用不需要定位功能,ACCESS_FINE_LOCATION权限也是必须的,因为BLE扫描可能被用于位置追踪。
3. BLE核心组件解析
3.1 BLE架构概述
Android BLE API主要包含以下几个关键类:
- BluetoothAdapter: 代表设备的蓝牙硬件
- BluetoothLeScanner: 用于扫描BLE设备
- BluetoothGatt: 处理GATT协议通信
- BluetoothGattCharacteristic: 表示GATT特征值
- BluetoothGattService: 表示GATT服务
理解这些组件的关系至关重要。一个BLE设备通常包含多个服务(Service),每个服务包含多个特征(Characteristic),每个特征包含值和描述符(Descriptor)。这种层级关系可以用以下结构表示:
code复制设备(Device)
├─ 服务1(Service)
│ ├─ 特征1(Characteristic)
│ │ ├─ 描述符1(Descriptor)
│ │ └─ 描述符2(Descriptor)
│ └─ 特征2(Characteristic)
└─ 服务2(Service)
└─ 特征3(Characteristic)
3.2 GATT协议详解
GATT(Generic Attribute Profile)是BLE通信的核心协议。它定义了服务(Service)和特征(Characteristic)的标准格式。每个服务和特征都有一个唯一的UUID标识:
- 标准UUID:16位短UUID,如0x180D代表心率服务
- 自定义UUID:128位长UUID,格式为0000XXXX-0000-1000-8000-00805F9B34FB
在实际开发中,你需要从设备厂商获取这些UUID定义。一个常见的错误是混淆了服务和特征的UUID,导致无法正确读写数据。
4. 设备扫描与连接实现
4.1 设备扫描最佳实践
使用Kotlin协程可以优雅地实现BLE扫描。以下是封装后的扫描代码:
kotlin复制class BleScanner(private val context: Context) {
private val bluetoothAdapter: BluetoothAdapter? by lazy {
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
private var scanningJob: Job? = null
private val foundDevices = mutableListOf<BluetoothDevice>()
fun startScan(
scanFilter: List<ScanFilter>? = null,
scanSettings: ScanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build(),
onDeviceFound: (BluetoothDevice) -> Unit
) {
if (bluetoothAdapter?.isEnabled != true) {
throw IllegalStateException("Bluetooth is not enabled")
}
scanningJob = CoroutineScope(Dispatchers.Main).launch {
val scanner = bluetoothAdapter?.bluetoothLeScanner
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
if (!foundDevices.contains(result.device)) {
foundDevices.add(result.device)
onDeviceFound(result.device)
}
}
}
try {
scanner?.startScan(scanFilter, scanSettings, callback)
// 自动停止扫描,避免耗电
delay(SCAN_PERIOD)
scanner?.stopScan(callback)
} catch (e: Exception) {
Log.e("BleScanner", "Scan failed", e)
}
}
}
fun stopScan() {
scanningJob?.cancel()
}
companion object {
private const val SCAN_PERIOD = 10000L // 10秒
}
}
提示:扫描是非常耗电的操作,应该限制扫描时间,并在找到目标设备后立即停止扫描。SCAN_MODE_LOW_LATENCY模式虽然响应快,但功耗最高,应根据实际需求选择合适的扫描模式。
4.2 设备连接与GATT操作
连接BLE设备是相对耗时的操作,应该在后台线程执行。以下是使用Kotlin协程封装的连接代码:
kotlin复制class BleConnector(private val context: Context) {
private var gatt: BluetoothGatt? = null
private var connectJob: Job? = null
suspend fun connect(device: BluetoothDevice): BluetoothGatt {
return suspendCancellableCoroutine { continuation ->
val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
Log.d("BleConnector", "Connected to ${device.address}")
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
Log.d("BleConnector", "Disconnected from ${device.address}")
continuation.resumeWithException(IOException("Disconnected"))
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
continuation.resume(gatt)
} else {
continuation.resumeWithException(IOException("Service discovery failed"))
}
}
}
gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
continuation.invokeOnCancellation {
gatt?.disconnect()
}
}
}
fun disconnect() {
connectJob?.cancel()
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
使用示例:
kotlin复制val connector = BleConnector(context)
try {
val gatt = withContext(Dispatchers.IO) {
connector.connect(device)
}
// 连接成功,可以操作服务
} catch (e: Exception) {
Log.e("BleConnector", "Connection failed", e)
}
5. 数据读写与通知处理
5.1 特征值读写操作
BLE设备的数据交互主要通过特征值(Characteristic)完成。以下是读写操作的封装实现:
kotlin复制suspend fun readCharacteristic(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
): ByteArray = suspendCoroutine { continuation ->
val callback = object : BluetoothGattCallback() {
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
continuation.resume(characteristic.value)
} else {
continuation.resumeWithException(IOException("Read failed"))
}
}
}
gatt.registerCallback(callback)
if (!gatt.readCharacteristic(characteristic)) {
continuation.resumeWithException(IOException("Read request failed"))
}
continuation.invokeOnCancellation {
gatt.unregisterCallback(callback)
}
}
suspend fun writeCharacteristic(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
data: ByteArray,
writeType: Int = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
): Unit = suspendCoroutine { continuation ->
val callback = object : BluetoothGattCallback() {
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
continuation.resume(Unit)
} else {
continuation.resumeWithException(IOException("Write failed"))
}
}
}
characteristic.value = data
characteristic.writeType = writeType
gatt.registerCallback(callback)
if (!gatt.writeCharacteristic(characteristic)) {
continuation.resumeWithException(IOException("Write request failed"))
}
continuation.invokeOnCancellation {
gatt.unregisterCallback(callback)
}
}
重要提示:WRITE_TYPE_DEFAULT和WRITE_TYPE_NO_RESPONSE的区别:
- DEFAULT:设备必须确认收到数据,可靠性高但速度慢
- NO_RESPONSE:设备不确认,速度快但可能丢失数据
5.2 通知与指示处理
接收设备主动发送的数据通常通过通知(Notification)或指示(Indication)实现。以下是设置通知的完整代码:
kotlin复制suspend fun enableNotification(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
): Flow<ByteArray> = callbackFlow {
val descriptor = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)
?: throw IOException("Descriptor not found")
val gattCallback = object : BluetoothGattCallback() {
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
trySend(characteristic.value).onFailure { throwable ->
Log.e("BleNotification", "Failed to send data", throwable)
}
}
override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) {
close(IOException("Enable notification failed"))
}
}
}
gatt.registerCallback(gattCallback)
// 启用本地通知
if (!gatt.setCharacteristicNotification(characteristic, true)) {
close(IOException("Set notification failed"))
return@callbackFlow
}
// 写入描述符启用通知
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (!gatt.writeDescriptor(descriptor)) {
close(IOException("Write descriptor failed"))
return@callbackFlow
}
awaitClose {
gatt.setCharacteristicNotification(characteristic, false)
gatt.unregisterCallback(gattCallback)
}
}
使用示例:
kotlin复制val dataFlow = enableNotification(gatt, characteristic)
dataFlow
.onEach { data ->
Log.d("BleData", "Received: ${data.toHexString()}")
}
.launchIn(viewModelScope)
6. 性能优化与常见问题
6.1 连接参数优化
BLE连接参数对性能和功耗影响很大。关键参数包括:
- 连接间隔(Connection Interval):1.25ms到4s之间,值越小响应越快但功耗越高
- 从设备延迟(Slave Latency):允许从设备跳过多少个连接事件
- 监督超时(Supervision Timeout):10ms到32s,检测连接丢失的超时时间
在Android 8.0及以上,可以通过以下方式请求优化参数:
kotlin复制fun requestConnectionPriority(gatt: BluetoothGatt, priority: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
gatt.requestConnectionPriority(priority)
}
}
// 使用示例
requestConnectionPriority(gatt, BluetoothGatt.CONNECTION_PRIORITY_HIGH)
6.2 常见问题排查
-
连接不稳定频繁断开
- 检查设备距离和干扰源
- 适当增加监督超时时间
- 确认设备电量充足
-
无法发现服务
- 确保已成功连接
- 检查是否调用了discoverServices()
- 确认设备UUID配置正确
-
写入失败
- 检查特征是否具有写入属性
- 尝试不同的写入类型(WRITE_TYPE_DEFAULT/WRITE_TYPE_NO_RESPONSE)
- 确认数据长度不超过最大传输单元(MTU)
-
通知不工作
- 确认特征支持通知/指示
- 检查是否成功写入描述符
- 验证是否调用了setCharacteristicNotification()
-
Android 12+扫描问题
- 确保已声明BLUETOOTH_SCAN权限
- 如果不需要定位,设置android:usesPermissionFlags="neverForLocation"
- 在运行时请求必要权限
7. 完整示例项目结构
一个健壮的BLE应用通常包含以下组件:
code复制com.example.bleapp
├── ble
│ ├── BleManager.kt // 核心BLE管理类
│ ├── BleScanner.kt // 扫描功能封装
│ ├── BleConnector.kt // 连接功能封装
│ └── model
│ ├── BleDevice.kt // 设备数据类
│ └── BleService.kt // 服务数据类
├── ui
│ ├── DeviceListFragment.kt // 设备列表界面
│ ├── DeviceDetailFragment.kt // 设备详情界面
│ └── viewmodel
│ ├── DeviceViewModel.kt // 设备列表ViewModel
│ └── DetailViewModel.kt // 设备详情ViewModel
└── utils
├── Extensions.kt // 扩展函数
└── Permissions.kt // 权限处理
BleManager作为核心类,可以这样实现:
kotlin复制class BleManager private constructor(private val context: Context) {
private val scanner = BleScanner(context)
private val connector = BleConnector(context)
private var currentGatt: BluetoothGatt? = null
companion object {
@Volatile
private var instance: BleManager? = null
fun getInstance(context: Context): BleManager {
return instance ?: synchronized(this) {
instance ?: BleManager(context.applicationContext).also { instance = it }
}
}
}
fun scanDevices(onFound: (BluetoothDevice) -> Unit) {
scanner.startScan(onDeviceFound = onFound)
}
suspend fun connect(device: BluetoothDevice): BluetoothGatt {
currentGatt?.disconnect()
return connector.connect(device).also { currentGatt = it }
}
fun disconnect() {
currentGatt?.disconnect()
currentGatt = null
}
suspend fun readCharacteristic(characteristic: BluetoothGattCharacteristic): ByteArray {
return currentGatt?.let { gatt ->
readCharacteristic(gatt, characteristic)
} ?: throw IllegalStateException("Not connected")
}
// 其他操作方法...
}
这种架构将BLE操作集中管理,便于维护和扩展,同时通过Kotlin协程简化异步操作。