Android蓝牙扫描权限的坑:为什么ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION必须同时申请?
1. 为什么Android蓝牙扫描需要位置权限第一次在Android上开发蓝牙功能时我遇到了一个诡异的问题明明已经在系统设置里开启了位置权限但就是扫描不到任何蓝牙设备。折腾了大半天才发现原来问题出在权限申请的细节上。Android从6.0API 23开始将蓝牙扫描功能与位置权限绑定。这背后的技术原理是蓝牙设备的MAC地址和信号强度RSSI可以被用来推算用户的位置。想象一下如果你知道某个蓝牙信标比如商场里的导航设备的固定位置再通过手机检测到它的信号强度就能大致判断用户与这个信标的距离。在Android 10及更高版本中这个要求变得更加严格。即使你只是想要扫描附近的蓝牙设备系统也会强制要求以下两个权限uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /这里有个关键细节系统设置里的位置权限开关实际上控制的是这两个权限的集合。但通过代码动态申请时它们却是完全独立的权限。这就是很多开发者踩坑的地方。2. 手动开启与动态申请的区别我在实际项目中遇到过这样的场景同一套代码在两台手机上表现完全不同。手机A能正常扫描蓝牙手机B却什么都扫不到。最后发现区别在于手机A我手动在系统设置里开启了位置权限手机B应用通过代码动态申请了ACCESS_COARSE_LOCATION权限关键区别当你在系统设置里手动开启位置权限时系统会同时授予ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION权限。但如果你只通过代码申请了其中一个权限系统设置里虽然显示位置权限已开启但实际上另一个权限并未被授予。这解释了为什么会出现权限已开却搜不到设备的诡异现象。Android的这种设计确实有点反直觉我猜Google是为了简化用户界面把两个权限在系统设置里合并显示了。3. 必须同时申请两个权限的原因经过反复测试我发现要确保蓝牙扫描在所有Android版本上正常工作必须同时检查并申请这两个权限。以下是具体原因版本差异Android 6.0-9.0ACCESS_COARSE_LOCATION足够Android 10必须使用ACCESS_FINE_LOCATIONAndroid 12可以使用新的BLUETOOTH_SCAN权限厂商定制 不同手机厂商对权限的处理可能有差异。有些厂商即使在中低版本Android上也会要求更精确的位置权限。未来兼容 同时申请两个权限可以确保应用在所有Android版本上都能正常工作避免出现某些设备无法扫描的情况。这里有个实际项目中的代码示例展示如何正确检查权限if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) ! PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { // 申请两个权限 ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }, REQUEST_CODE_LOCATION_PERMISSION); }4. Android 12的新变化从Android 12API 31开始Google引入了更细粒度的蓝牙权限体系终于把蓝牙权限和位置权限解耦了。新权限包括BLUETOOTH_SCAN用于发现周边设备BLUETOOTH_CONNECT用于连接已配对设备BLUETOOTH_ADVERTISE用于广播设备如果你只做蓝牙扫描可以这样声明uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation /添加neverForLocation标记后应用就不需要位置权限也能扫描蓝牙设备了。这是解决这个历史遗留问题的最佳方案。不过在实际开发中我们还需要考虑兼容旧版本!-- 新权限 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / !-- 旧权限maxSdkVersion限制只在低版本使用 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION android:maxSdkVersion30 /5. 实际开发中的解决方案结合我多个项目的经验总结出一个可靠的解决方案AndroidManifest.xml配置!-- 新蓝牙权限Android 12 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.BLUETOOTH_ADVERTISE / !-- 旧位置权限Android 6-11 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION android:maxSdkVersion30 / !-- 声明蓝牙功能 -- uses-feature android:nameandroid.hardware.bluetooth_le android:requiredtrue /运行时权限检查逻辑private boolean checkBluetoothPermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // Android 12 使用新权限 return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) PackageManager.PERMISSION_GRANTED; } else { // Android 6-11 检查位置权限 return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) PackageManager.PERMISSION_GRANTED; } }权限申请逻辑private void requestBluetoothPermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_SCAN}, REQUEST_CODE_BLUETOOTH_PERMISSION); } else { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }, REQUEST_CODE_LOCATION_PERMISSION); } }6. 用户拒绝权限时的处理即使用户拒绝了位置权限我们也不应该让应用完全无法使用。好的做法是解释为什么需要权限我们需要位置权限来发现附近的蓝牙设备不会追踪您的实际位置提供手动重试的选项对于非核心功能可以降级处理示例代码Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQUEST_CODE_LOCATION_PERMISSION) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { // 权限已授予开始扫描 startBluetoothScan(); } else { // 解释为什么需要这个权限 new AlertDialog.Builder(this) .setTitle(需要位置权限) .setMessage(蓝牙设备扫描需要位置权限来发现附近的设备我们不会收集或存储您的位置信息) .setPositiveButton(再试一次, (dialog, which) - requestBluetoothPermissions()) .setNegativeButton(取消, null) .show(); } } }7. 测试时的注意事项在测试蓝牙功能时我发现以下几个实用技巧清除权限状态测试不同权限场景时记得在应用设置里完全清除权限而不仅仅是关闭多设备测试不同厂商设备对权限的处理可能有差异ADB命令可以通过ADB快速授予/撤销权限adb shell pm grant package_name android.permission.ACCESS_FINE_LOCATION adb shell pm revoke package_name android.permission.ACCESS_FINE_LOCATIONAndroid 10的特殊性记得测试GPS开关的影响因为Android 10后蓝牙扫描还要求GPS是开启状态8. 总结与最佳实践经过多次项目实践我总结出以下最佳实践同时申请两个位置权限在Android 11及以下版本中同时检查并申请ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION适配Android 12使用新的BLUETOOTH_SCAN权限并添加neverForLocation标记清晰的用户引导解释为什么需要这些权限降低用户拒绝的概率优雅降级当权限被拒绝时提供合理的备选方案全面测试在不同Android版本和厂商设备上测试权限相关功能蓝牙开发中的权限问题确实是个坑但理解了背后的原理和Android的设计意图后就能写出更健壮的代码。希望我的这些经验能帮你少走弯路。

相关新闻