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开发------优化
相关推荐
姜行运1 小时前
数据结构【二叉搜索树(BST)】
android·数据结构·c++·c#
JhonKI9 小时前
【MySQL】存储引擎 - CSV详解
android·数据库·mysql
开开心心_Every9 小时前
手机隐私数据彻底删除工具:回收或弃用手机前防数据恢复
android·windows·python·搜索引擎·智能手机·pdf·音视频
大G哥10 小时前
Kotlin Lambda语法错误修复
android·java·开发语言·kotlin
鸿蒙布道师13 小时前
鸿蒙NEXT开发动画案例2
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
androidwork13 小时前
Kotlin Android工程Mock数据方法总结
android·开发语言·kotlin
xiangxiongfly91515 小时前
Android setContentView()源码分析
android·setcontentview
悠哉清闲16 小时前
kotlin一个函数返回多个值
kotlin
人间有清欢16 小时前
Android开发补充内容
android·okhttp·rxjava·retrofit·hilt·jetpack compose
人间有清欢17 小时前
Android开发报错解决
android