Android_BLE开发——扫描

本文结合 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次,将会暂时关闭扫描。

更多分享

  1. Android_BLE开发------初识BLE
  2. Android_BLE开发------连接
  3. Android_BLE开发------读写
  4. Android_BLE开发------绑定
  5. Android_BLE开发------优化
相关推荐
woodWu44 分钟前
Android编译时动态插入代码原理与实践
android
百锦再2 小时前
Android Studio 实现自定义全局悬浮按钮
android·java·ide·app·android studio·安卓
百锦再2 小时前
Android Studio 项目文件夹结构详解
android·java·ide·ios·app·android studio·idea
老码识土2 小时前
Kotlin 协程源代码泛读:Continuation
android·kotlin
行墨4 小时前
Replugin 的hook点以及hook流程
android·架构
一一Null4 小时前
Access Token 和 Refresh Token 的双令牌机制,维持登陆状态
android·python·安全·flask
iOS大前端海猫4 小时前
深入解析 Swift 中的并发属性包装器:@Actor、@MainActor 和 @GlobalActor
ios·app
_祝你今天愉快4 小时前
深入理解 Android Handler
android
louisgeek4 小时前
Kotlin 挂起函数的原理
kotlin
pengyu5 小时前
【Flutter 状态管理 - 四】 | setState的工作机制探秘
android·flutter·dart