本文结合 BLE 开发核心原理与实战经验,深入剖析 Android 蓝牙扫描的实现细节,涵盖权限适配、扫描策略优化、兼容性处理等关键问题,助你打造高效稳定的扫描模块。
新手推荐开源库 :FastBle
一、BLE 扫描核心实现
kotlin
class BleScanner(private val context: Context) {
private val bluetoothAdapter: BluetoothAdapter? by lazy {
BluetoothAdapter.getDefaultAdapter()
}
private var scanner: BluetoothLeScanner? = null
private var scanJob: Job? = null
// 使用密封类封装扫描结果
sealed class ScanState {
object Started : ScanState()
object Stopped : ScanState()
data class FoundDevice(val device: BluetoothDevice, val rssi: Int) : ScanState()
}
// 启动扫描(带超时控制)
fun startScan(
filters: List<ScanFilter> = emptyList(),
settings: ScanSettings = defaultScanSettings(),
timeoutMillis: Long = 10_000
): Flow<ScanState> = callbackFlow {
scanner = bluetoothAdapter?.bluetoothLeScanner ?: return@callbackFlow
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
trySend(ScanState.FoundDevice(result.device, result.rssi))
}
}
trySend(ScanState.Started)
scanner?.startScan(filters, settings, callback)
// 自动超时停止
scanJob = launch {
delay(timeoutMillis)
stopScan()
}
awaitClose { stopScan() }
}
private fun stopScan() {
scanner?.stopScan()
scanJob?.cancel()
scanner = null
}
private fun defaultScanSettings() = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
}
二、权限处理与兼容性适配
2.1、权限矩阵
权限 | Android 6.0+ | Android 12+ | 备注 |
---|---|---|---|
BLUETOOTH |
✅ | ✅ | 基础蓝牙权限 |
BLUETOOTH_ADMIN |
✅ | ✅ | 管理蓝牙连接 |
ACCESS_FINE_LOCATION |
✅ | ❌ | 仅扫描时需要 |
BLUETOOTH_SCAN |
❌ | ✅ | 替代位置权限 |
BLUETOOTH_CONNECT |
❌ | ✅ | 连接设备需要 |
2.2、动态权限(示例代码)
kotlin
// 权限检查扩展函数
fun Activity.checkBlePermissions(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PERMISSION_GRANTED &&
checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PERMISSION_GRANTED
} else {
checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PERMISSION_GRANTED
}
}
// 权限请求封装
fun Activity.requestBlePermissions(requestCode: Int) {
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
} else {
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
)
}
requestPermissions(permissions, requestCode)
}
2.3、Android 12+ 适配配置
- 必须在
AndroidManifest.xml
声明新权限 - 扫描时需要添加
Android:usesPermissionFlags="neverForLocation"
声明
xml
<!-- AndroidManifest.xml -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
三、扫描优化策略
3.1、性能优化维度
策略 | 实现方式 | 适用场景 |
---|---|---|
扫描周期分片 | 扫描 3s → 停止 1s → 循环 | 平衡功耗与效率 |
信号强度过滤 | 通过 RSSI 值过滤远距离设备 | 精准定位目标 |
设备名称/服务UUID过滤 | 使用 ScanFilter 预筛选 | 减少无效回调 |
3.2、设备去重与缓存(示例代码)
kotlin
fun scanDevices(): Flow<BluetoothDevice> = flow {
val deviceCache = mutableSetOf<String>()
bleScanner.startScan().collect { state ->
when (state) {
is ScanState.FoundDevice -> {
val address = state.device.address
if (!deviceCache.contains(address)) {
deviceCache.add(address)
emit(state.device)
}
}
else -> Unit
}
}
}.buffer(50) // 添加背压处理
3.3、广播数据解析(示例代码)
kotlin
// 解析广播数据扩展函数
fun ScanRecord.parseManufacturerData(companyId: Int): ByteArray? {
return manufacturerSpecificData[companyId]
}
// 解析 iBeacon 数据
fun ScanRecord.parseIBeacon(): IBeacon? {
val bytes = bytes ?: return null
if (bytes.size < 25) return null
return with(bytes) {
if (get(7).toInt() == 0x02 && get(8).toInt() == 0x15) {
IBeacon(
uuid = copyOfRange(9, 25).toHexString(),
major = (get(25).toUByte().toInt() shl 8 or get(26).toUByte().toInt(),
minor = (get(27).toUByte().toInt() shl 8 or get(28).toUByte().toInt(),
txPower = get(29).toInt()
)
} else null
}
}
// 数据类封装
data class IBeacon(
val uuid: String,
val major: Int,
val minor: Int,
val txPower: Int
)
解析 iBeacon 协议 java版本
java
// iBeacon 数据格式解析
byte[] bytes = record.getBytes();
if (bytes[7] == 0x02 && bytes[8] == 0x15) { // iBeacon 标识
String uuid = bytesToHex(Arrays.copyOfRange(bytes, 9, 25));
int major = (bytes[25] & 0xff) << 8 | (bytes[26] & 0xff);
int minor = (bytes[27] & 0xff) << 8 | (bytes[28] & 0xff);
int txPower = bytes[29];
}
四、实战避坑指南和解决方案
4.1、后台扫描限制处理(示例代码)
- Android 8.0+ :后台应用每秒最多扫描 5 次
kotlin
// 使用前台服务实现持续扫描
class BleScanService : Service() {
private val notificationId = 1001
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(notificationId, createNotification())
startBackgroundScan()
return START_STICKY
}
private fun startBackgroundScan() {
val pendingIntent = PendingIntent.getBroadcast(
this, 0,
Intent(this, ScanReceiver::class.java),
PendingIntent.FLAG_MUTABLE
)
bluetoothAdapter.bluetoothLeScanner.startScan(null, settings, pendingIntent)
}
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, "ble_channel")
.setContentTitle("BLE Scanning")
.setSmallIcon(R.drawable.ic_scan)
.build()
}
}
4.2、扫描模式智能切换(示例代码)
扫描模式对比:
扫描模式 | 功耗 | 发现速度 | 适用场景 |
---|---|---|---|
SCAN_MODE_LOW_POWER |
最低 | 慢 | 后台持续扫描 |
SCAN_MODE_BALANCED |
中等 | 中 | 一般场景 |
SCAN_MODE_LOW_LATENCY |
高 | 快 | 需要快速响应 |
kotlin
// 根据应用状态切换扫描模式
enum class ScanStrategy {
FOREGROUND_FAST,
BACKGROUND_LOW_POWER,
BALANCED
}
fun getScanSettings(strategy: ScanStrategy): ScanSettings {
return when (strategy) {
FOREGROUND_FAST -> ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
BACKGROUND_LOW_POWER -> ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.build()
BALANCED -> ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.build()
}
}
五、最佳实践建议
5.1、协程管理生命周期
- 使用
CoroutineScope
绑定界面生命周期,避免内存泄漏:
kotlin
lifecycleScope.launch {
bleScanner.startScan().collect { /* 处理结果 */ }
}
5.2、设备信息持久化
- 使用
Room
数据库缓存扫描结果:
kotlin
@Entity
data class ScannedDevice(
@PrimaryKey val macAddress: String,
val name: String?,
val lastSeen: Long,
val rssi: Int
)
5.3、响应式界面更新
- 结合
Jetpack Compose
实现实时列表:
kotlin
@Composable
fun DeviceList(devices: List<BluetoothDevice>) {
LazyColumn {
items(devices) { device ->
DeviceItem(device)
}
}
}
5.4、建议结合 Android BLE KTX 官方扩展库进一步简化开发流程。
六、Google关于持续性扫描做的限制
6.1、Android 8.1
未设置Scan Filter,手机熄屏会停止扫描。
6.2、Android 7
Undocumented Android 7 BLE Behavior Changes
- 只能持续扫描30分钟,之后ScanSettings会被改为SCAN_MODE_OPPORTUNISTIC.
- 30s内,重复开启扫描5次,将会暂时关闭扫描。