
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以及后臺限制相關的問題。