Android 12蓝牙权限适配指南:解决BLUETOOTH_SCAN失效与后台扫描限制

📅 2026/8/26 4:35:11
Android 12蓝牙权限适配指南:解决BLUETOOTH_SCAN失效与后台扫描限制
1. 项目概述当蓝牙扫描在Android 12上“失灵”如果你最近将你的Android应用升级到以Android 12API级别31为目标版本或者你的用户设备升级到了Android 12突然发现一个老功能——蓝牙设备扫描——不工作了那么你绝对不是一个人。这个看似简单的功能背后是Android权限模型又一次重大的演进。过去在Android 6.0之后我们习惯了在运行时请求危险权限比如ACCESS_FINE_LOCATION来让蓝牙扫描正常工作。但到了Android 12谷歌引入了更细粒度的蓝牙权限控制新增了BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE和BLUETOOTH_CONNECT三个权限彻底改变了游戏规则。很多开发者包括经验丰富的我都在这里踩了坑明明代码没变权限也申请了但BluetoothAdapter.startDiscovery()或者BluetoothLeScanner.startScan()就是返回空列表或者直接失败。这篇笔记就是记录我如何从“一脸懵”到彻底搞懂并解决这个问题的全过程涵盖了从权限声明、运行时请求到后台扫描限制的所有核心细节。2. Android 12蓝牙权限体系深度解析Android 12的蓝牙权限改革核心思想是“最小权限原则”和“用户隐私保护”的进一步落地。它将过去相对粗放的蓝牙操作权限拆解成了意图更明确、控制更精细的三个独立权限。理解它们各自的职责是解决问题的第一步。2.1 新旧权限体系对比与映射在Android 11API 30及之前进行蓝牙扫描特别是BLE扫描通常需要两个权限BLUETOOTH和BLUETOOTH_ADMIN这两个是普通权限在AndroidManifest.xml中声明即可用于基础的蓝牙操作和管理。ACCESS_FINE_LOCATION这是一个危险权限因为通过扫描蓝牙信号特别是BLE信标可以推断设备的地理位置。所以从Android 6.0开始扫描蓝牙设备需要你在运行时向用户申请这个位置权限。到了Android 12情况变得复杂也清晰了BLUETOOTH_SCAN这是本次问题的核心。它取代了ACCESS_FINE_LOCATION在扫描场景下的作用。任何希望发现周边蓝牙设备包括经典蓝牙和BLE的应用都必须申请此权限。BLUETOOTH_CONNECT用于连接已经配对或发现的蓝牙设备例如连接耳机、向设备写入数据等。BLUETOOTH_ADVERTISE用于让本设备作为外围设备广播自身被其他设备发现。对于需要向后兼容的应用即targetSdkVersion 31但在Android 12设备上运行系统会尝试进行权限映射。但最可靠的方式还是主动适配新的权限体系。注意ACCESS_FINE_LOCATION权限在Android 12上对于蓝牙扫描依然可能被需要但前提是你的应用在声明BLUETOOTH_SCAN权限时没有添加android:usesPermissionFlagsneverForLocation属性。如果你扫描的目的是为了获取位置如室内导航你就需要它如果只是为了连接设备如智能硬件你可以声明不需要位置信息从而避免申请位置权限提升用户通过率。2.2BLUETOOTH_SCAN权限的特殊属性与FlagsBLUETOOTH_SCAN不是一个简单的危险权限。它有几个关键属性必须在AndroidManifest.xml中声明清楚android:permissionFlags这个属性至关重要。neverForLocation如果你能向系统和用户证明你的应用扫描蓝牙设备不是为了获取地理位置例如你只是为了连接一个已知的智能灯泡或体重秤那么你应该加上这个标志。加上后你的应用将不再需要ACCESS_FINE_LOCATION权限。这是符合谷歌隐私政策的最佳实践。声明示例uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation /maxSdkVersion为了优雅地处理向后兼容性你通常需要为旧权限设置最高SDK版本。对于ACCESS_FINE_LOCATION如果你使用了neverForLocation标志理论上在Android 12上就不需要它了。但为了兼容Android 11及以下的设备你仍然需要声明它并限制其最高版本。声明示例uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 /这意味着在Android 12API 31及以上的设备上系统会忽略这个权限声明。2.3 后台扫描限制另一个潜在的“坑”即使你正确声明和请求了BLUETOOTH_SCAN权限在Android 12上蓝牙扫描行为还受到应用可见性App Visibility和后台模式的严格限制。前台服务要求如果你的应用在后台例如没有可见的Activity执行蓝牙扫描必须要有一个带有foregroundServiceTypeconnectedDevice的前台服务正在运行。这是Android 10引入、在后续版本中强化的限制。没有前台服务时的行为如果没有正确的前台服务即使拥有BLUETOOTH_SCAN权限后台扫描也可能扫描结果回调延迟极大。扫描不到任何设备。在onScanFailed回调中收到错误码SCAN_FAILED_APPLICATION_REGISTRATION_FAILED。因此一个健壮的蓝牙扫描功能需要同时处理好权限和运行时状态前台/后台两套逻辑。3. 完整解决方案与代码实操理论讲清楚了我们来看具体怎么改。假设我们有一个简单的BLE扫描应用需要适配Android 12。3.1 第一步修正 AndroidManifest.xml这是所有工作的基础必须准确无误。?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.bleapp !-- 对于 Android 12 (API 31) 的蓝牙扫描权限 -- !-- 添加 neverForLocation 标志表明我们扫描不是为了获取地理位置 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / !-- 对于 Android 12 的蓝牙连接权限如果需要连接设备 -- uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / !-- 兼容 Android 6.0 到 11 的位置权限并限制最高版本 -- !-- maxSdkVersion30 意味着此权限在 Android 12 设备上不会被请求 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 / !-- 旧版蓝牙管理权限普通权限声明即可 -- uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / !-- 如果需要在后台扫描必须声明前台服务类型 -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / application ... !-- 后台蓝牙扫描所需的前台服务声明 -- service android:name.BluetoothScanForegroundService android:foregroundServiceTypeconnectedDevice android:exportedfalse / ... /application /manifest3.2 第二步实现动态权限请求逻辑在你的Activity或Fragment中你需要编写逻辑来判断设备版本并请求正确的权限。import android.Manifest import android.bluetooth.BluetoothAdapter import android.bluetooth.le.BluetoothLeScanner import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.content.pm.PackageManager import android.os.Build import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat class MainActivity : AppCompatActivity() { private lateinit var bluetoothAdapter: BluetoothAdapter private lateinit var bluetoothLeScanner: BluetoothLeScanner private val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { // 处理扫描到的设备 val device result.device val rssi result.rssi // ... 更新UI } override fun onScanFailed(errorCode: Int) { // 扫描失败处理 when (errorCode) { ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED - { // 常见于后台扫描没有前台服务或权限问题 } ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED - {} ScanCallback.SCAN_FAILED_INTERNAL_ERROR - {} ScanCallback.SCAN_FAILED_ALREADY_STARTED - {} } } } // 使用 Activity Result API 请求权限 private val requestPermissionLauncher registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions - val allGranted permissions.all { it.value } if (allGranted) { // 所有必要权限都已授予开始扫描 startBluetoothScan() } else { // 向用户解释为什么需要这些权限 showPermissionDeniedDialog() } } private fun checkAndRequestPermissions() { val permissionsToRequest mutableListOfString() // 检查并添加 BLUETOOTH_SCAN 权限 (Android 12) if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) ! PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(Manifest.permission.BLUETOOTH_SCAN) } // 如果需要连接设备还需要 BLUETOOTH_CONNECT if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) ! PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT) } } else { // Android 11 及以下需要位置权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION) } } // 检查蓝牙开关状态这是一个运行时状态不是权限 bluetoothAdapter BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter null || !bluetoothAdapter.isEnabled) { // 提示用户打开蓝牙 val enableBtIntent Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) return } if (permissionsToRequest.isNotEmpty()) { // 请求缺失的权限 requestPermissionLauncher.launch(permissionsToRequest.toTypedArray()) } else { // 已有所有权限直接开始扫描 startBluetoothScan() } } private fun startBluetoothScan() { bluetoothLeScanner bluetoothAdapter.bluetoothLeScanner val scanSettings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() // 可以添加 ScanFilter 来过滤特定设备 val filters listOfScanFilter() // 为空则扫描所有设备 // 检查是否在后台。如果是需要确保前台服务已启动。 if (isAppInBackground()) { // 启动一个带有 connectedDevice 类型的前台服务 val serviceIntent Intent(this, BluetoothScanForegroundService::class.java) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(serviceIntent) } else { startService(serviceIntent) } // 通常前台服务内部会调用 startScan } else { // 在前台直接扫描 bluetoothLeScanner.startScan(filters, scanSettings, scanCallback) } } // ... 其他代码如停止扫描、处理生命周期等 }3.3 第三步处理后台扫描前台服务实现BluetoothScanForegroundService.kt示例import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.bluetooth.le.BluetoothLeScanner import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.content.Intent import android.os.Build import android.os.IBinder class BluetoothScanForegroundService : Service() { private lateinit var bluetoothLeScanner: BluetoothLeScanner private val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { // 后台处理扫描结果例如存入数据库或通过 WorkManager 上传 } } override fun onCreate() { super.onCreate() val bluetoothAdapter BluetoothAdapter.getDefaultAdapter() bluetoothLeScanner bluetoothAdapter.bluetoothLeScanner startForegroundScan() } private fun startForegroundScan() { // 1. 创建通知渠道 (Android O及以上) if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { val channelId bluetooth_scan_channel val channelName 蓝牙扫描服务 val importance NotificationManager.IMPORTANCE_LOW val channel NotificationChannel(channelId, channelName, importance).apply { description 用于保持后台蓝牙设备扫描 } val notificationManager getSystemService(NotificationManager::class.java) notificationManager.createNotificationChannel(channel) } // 2. 构建通知 val notification if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { Notification.Builder(this, bluetooth_scan_channel) } else { Notification.Builder(this) }.apply { setContentTitle(蓝牙设备扫描中) setContentText(正在搜索附近的蓝牙设备...) setSmallIcon(android.R.drawable.ic_dialog_info) // 使用你自己的图标 setOngoing(true) }.build() // 3. 启动前台服务 startForeground(1, notification) // 4. 开始蓝牙扫描 val filters listOfScanFilter() val scanSettings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 后台建议低功耗模式 .build() bluetoothLeScanner.startScan(filters, scanSettings, scanCallback) } override fun onDestroy() { bluetoothLeScanner.stopScan(scanCallback) super.onDestroy() } override fun onBind(intent: Intent?): IBinder? null }4. 常见问题排查与实战心得即使代码看起来都对了在实际调试中依然会遇到各种“妖魔鬼怪”。下面是我在真机调试和用户反馈中收集的典型问题及解决方法。4.1 问题速查表现象可能原因排查步骤与解决方案扫描回调onScanResult从未被调用1.BLUETOOTH_SCAN权限未在Manifest中声明或声明错误。2. 权限未在运行时授予。3. 应用在后台且未启动正确的前台服务。4. 设备蓝牙未开启。1. 检查AndroidManifest.xml确保uses-permission android:nameandroid.permission.BLUETOOTH_SCAN ... /存在且正确。2. 在设置中查看应用权限列表确认BLUETOOTH_SCAN和BLUETOOTH_CONNECT若需要已开启。3. 使用adb shell dumpsys package your.package.name命令查看应用权限状态。4. 确保蓝牙已打开并尝试扫描系统蓝牙设置中能看到的设备。onScanFailed回调返回SCAN_FAILED_APPLICATION_REGISTRATION_FAILED这是Android 12后台扫描的典型错误。应用在后台状态尝试扫描但没有一个活跃的、类型为connectedDevice的前台服务。1. 确保你的扫描代码在应用退到后台时已经启动了一个前台服务。2. 检查服务声明中的android:foregroundServiceTypeconnectedDevice。3. 检查通知渠道和通知是否成功创建并显示。在Android 11设备上工作正常升级到Android 12后扫描不到设备1. 应用targetSdkVersion已升级到31但未适配新权限。2. 用户升级系统后应用权限被重置或需要重新授权。1. 确认应用已按照上文正确声明和请求BLUETOOTH_SCAN权限。2. 引导用户到系统设置中手动为你的应用开启“附近设备”权限在某些厂商定制UI中BLUETOOTH_SCAN可能被归类于此。3. 在代码中增加权限检查如果未授权则再次引导用户。能扫描到部分设备但扫描不到目标设备1. 目标设备未处于可发现模式如BLE未广播。2. 扫描设置ScanSettings问题例如扫描模式过于保守。3. 手机系统或目标设备存在省电策略限制。1. 确认目标设备正常工作并处于广播状态。可以用其他蓝牙扫描App如nRF Connect交叉验证。2. 尝试使用ScanSettings.SCAN_MODE_LOW_LATENCY高功耗模式进行测试。3. 关闭手机的省电模式并将你的应用加入电池优化白名单。权限弹窗显示混乱或请求了不需要的权限1.AndroidManifest.xml中权限声明冗余或冲突。2. 动态请求权限的列表逻辑有误。1. 清理Manifest移除不必要的权限声明。确保ACCESS_FINE_LOCATION有android:maxSdkVersion30限制。2. 仔细检查checkAndRequestPermissions()函数中的版本判断逻辑确保只在对应版本请求对应权限。4.2 实操心得与避坑指南真机调试是唯一真理Android模拟器对蓝牙的支持一直不完美尤其是权限和硬件交互部分。务必使用Android 12或更高版本的实体手机进行开发和测试。在开发者选项中可以模拟权限被拒绝的场景。使用ADB命令快速验证权限状态这是最高效的调试手段之一。连接手机后在终端执行adb shell dumpsys package com.your.package.name | grep -A 50 Permissions:查看输出中android.permission.BLUETOOTH_SCAN和android.permission.BLUETOOTH_CONNECT的状态是grantedtrue还是false。“附近设备”权限的坑部分国产定制系统如MIUI、ColorOS将BLUETOOTH_SCAN权限包装在了一个叫“附近设备”的权限组里。用户可能在系统设置里找不到独立的“蓝牙扫描”开关。你需要引导用户到“应用管理 - 你的应用 - 权限管理”里找到“附近设备”并允许。在代码中弹窗解释时最好提及这一点。后台服务通知不能马虎Android系统对前台服务的通知检查非常严格。通知渠道必须创建通知必须显示且不能轻易被用户清除setOngoing(true)。如果通知没展示出来前台服务可能被系统杀死导致后台扫描立即停止。neverForLocation标志的权衡加上这个标志可以避免申请敏感的位置权限对用户更友好审核也更容易通过。但前提是你的应用真的不需要位置信息。如果你的应用功能是基于地理围栏或室内定位去掉这个标志并申请位置权限是更诚实的做法。升级targetSdkVersion要谨慎当你将应用的targetSdkVersion升级到31或更高时意味着你明确告诉系统你的应用已完全适配此版本。系统将严格执行新规如后台扫描限制。建议在升级前在分支上进行充分的兼容性测试处理好所有BLUETOOTH_SCAN、BLUETOOTH_CONNECT以及后台限制相关的问题。