16. Android 12+ BLE适配:蓝牙权限变更、精确位置要求、扫描过滤限制、兼容性处理
Android 12 这一版,说实话,对 BLE 开发者来说是个分水岭。我当年第一次在 Android 12 设备上跑老项目时,直接闪退加扫描不到设备,搞得我一头雾水。后来翻文档才发现,Google 把蓝牙权限体系整个重构了一遍。
这一章,我就把我在适配过程中踩过的坑、总结的经验,一次性讲清楚。
16.1 蓝牙权限的「三权分立」
Android 12 之前,你只需要 BLUETOOTH 和 BLUETOOTH_ADMIN 两个权限就能跑 BLE。但 12 之后,Google 引入了三个新运行时权限:
| 权限名 | 用途 | 是否需要运行时申请 |
|---|---|---|
BLUETOOTH_SCAN |
扫描 BLE 设备 | 是 |
BLUETOOTH_CONNECT |
连接已扫描到的设备 | 是 |
BLUETOOTH_ADVERTISE |
广播 BLE 信号(外设模式) | 是 |
嗯,这里要注意:这三个权限是独立的。你只申请 SCAN 权限,是没法调用 connectGatt() 的。我见过不少同事只申请了 SCAN,结果连接时直接抛 SecurityException。
核心原则:扫描用 SCAN,连接用 CONNECT,广播用 ADVERTISE。别偷懒,三个都申请最稳妥。
16.2 精确位置要求——终于松绑了
Android 12 之前,扫描 BLE 必须申请 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION。这其实是个历史遗留问题——因为 BLE 扫描可以用来做粗略定位。
但从 Android 12 开始,如果你不需要基于 BLE 做定位,可以不申请位置权限。怎么告诉系统?在清单文件里加一行:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
加上 neverForLocation 标志后,系统就知道你的扫描不用于定位,也就不会弹位置权限的对话框了。
警告:如果你在代码里调用了 startScan() 但没有声明 neverForLocation,系统仍然会要求位置权限。而且 Google Play 审核时会检查你的应用是否真的需要位置信息——别乱加这个标志。
我个人习惯是:如果我的 App 只是连接已知设备(比如智能手表),我就加 neverForLocation。如果 App 需要根据设备位置做推荐,那就不加,老老实实申请位置权限。
16.3 扫描过滤限制——Google 的「保护」有点过头
Android 12 引入了一个新限制:后台扫描会被限频。具体来说,如果你的 App 在后台(没有前台 Service 或 Activity),每 30 分钟只能扫描 4 次。每次扫描最长 30 秒。
为什么会这样?Google 说这是为了省电和保护隐私。但说实话,这对很多 IoT 应用来说简直是灾难。我曾经做过一个仓库资产追踪 App,需要在后台持续扫描 BLE 标签。Android 12 一上,扫描直接废了。
解决办法有两个:
- 方案一:启动一个前台 Service,并声明
BLUETOOTH_SCAN权限对应的通知。这样系统会认为你的扫描是「前台行为」,不受限频影响。 - 方案二:使用
BluetoothLeScanner的startScan()时,传入ScanSettings并设置SCAN_MODE_LOW_LATENCY。但这只能提高扫描频率,不能突破后台限制。
// 前台 Service 示例
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
// 在 onStartCommand 中启动扫描
bluetoothLeScanner.startScan(filters, scanSettings, scanCallback)
小技巧:如果你只是做「扫描-连接-断开」这种一次性操作,可以考虑用 BluetoothAdapter 的 startDiscovery() 替代。但注意,这个方法返回的是经典蓝牙设备,不是 BLE。
16.4 兼容性处理——让老设备也能跑
你的 App 肯定要兼容 Android 11 及以下的设备。这时候就需要做版本判断。我一般这样写:
// 在 Application 或 Activity 中
private fun requestBluetoothPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+:申请新权限
requestPermissions(
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_ADVERTISE
),
REQUEST_CODE_BLE_PERMISSIONS
)
} else {
// Android 11 及以下:申请位置权限
requestPermissions(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION
),
REQUEST_CODE_LOCATION_PERMISSIONS
)
}
}
另外,清单文件里也要做兼容声明:
<!-- Android 12+ 权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
android:maxSdkVersion="33" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"
android:maxSdkVersion="33" />
<!-- Android 11 及以下 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<!-- 基础权限(所有版本都需要) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
注意 maxSdkVersion 的用法。这样在 Android 12+ 设备上,系统不会去检查位置权限,避免不必要的弹窗。
16.5 知识体系总览
下面这张图是我自己整理的 Android 12+ BLE 适配核心逻辑,你可以对照着看:
16.6 避坑指南
最后,分享几个我实际项目中踩过的坑:
- 权限申请顺序:先申请 SCAN,再申请 CONNECT。如果反过来,系统可能会认为你的 App 行为异常。我遇到过用户反馈「点了允许但连接失败」,就是因为顺序反了。
- 模拟器测试:Android 12 模拟器对 BLE 支持很差。我建议直接用真机测试。曾经在模拟器上跑了一整天,所有权限都正常,结果真机上一运行就崩——因为模拟器没有真正的蓝牙硬件。
- targetSdkVersion:如果你的 targetSdkVersion 低于 31,系统会以兼容模式运行,不会强制要求新权限。但 Google Play 从 2022 年开始要求新应用必须 target 31+。所以别想着靠低版本绕过去。
- 权限撤销:Android 12 允许用户随时撤销运行时权限。你的 App 要能处理权限被撤销的情况——比如在 onResume 里检查权限状态,如果被撤销就重新申请。
个人建议:写一个统一的权限管理类,把 Android 12+ 和 Android 11- 的逻辑封装好。这样以后升级到 Android 14、15 时,只需要改这个类就行。我自己的项目就是这么做的,省了不少事。
好了,Android 12+ 的 BLE 适配就讲到这里。核心就是三件事:权限分家、位置松绑、后台限频。把这三点处理好,你的 App 就能在 Android 12 到 14 上平稳运行。
公众号:蓝海资料掘金营,微信deep3321