16、蓝牙后台扫描限制:Android 8+后台限制、Android 10+位置限制、前台服务保活方案、WorkManager定时扫描

做蓝牙开发这么多年,后台扫描这块坑是真的多。我记得刚接手一个蓝牙门禁项目时,用户反馈说手机锁屏几分钟后就扫描不到设备了。我一开始还以为是硬件问题,查了半天才发现——嗯,是系统把我们的扫描给掐了。

从 Android 8 开始,Google 对后台扫描的限制越来越严。说白了,系统觉得你一个 App 在后台偷偷扫描蓝牙,既耗电又侵犯隐私。但咱们做 IoT 或室内定位的,后台扫描又是刚需。怎么办?今天我就把这几年的踩坑经验掰开揉碎讲给你听。

核心结论:Android 8+ 限制后台扫描频率,Android 10+ 要求位置权限,前台服务是目前最稳的保活方案,WorkManager 适合非实时性扫描任务。

蓝牙后台扫描限制知识体系 蓝牙后台扫描 Android 8+ 后台限制 Android 10+ 位置限制 前台服务保活 WorkManager ScanFilter 限制 ACCESS_FINE_LOCATION 前台服务 + 通知 定时周期性扫描 最佳实践:前台服务 + 位置权限 + 合理扫描间隔

16.1 Android 8+ 后台扫描限制:系统到底在防什么?

Android 8.0(API 26)引入了一个重要变化:当 App 进入后台后,蓝牙扫描会被系统自动限制。具体来说,系统会缓存扫描结果,然后批量上报,而不是实时回调。

我当初调试一个蓝牙温度计项目时,发现 App 切到后台后,温度数据要等好几分钟才更新一次。一开始我还以为是蓝牙模块坏了,后来查了官方文档才恍然大悟——原来是被系统限流了。

Android 版本 后台扫描行为 影响范围
Android 8.0 (API 26) 扫描结果缓存,批量上报 所有后台 App
Android 9.0 (API 28) 后台扫描频率进一步降低 未使用前台服务的 App
Android 10+ (API 29) 增加位置权限要求 所有蓝牙扫描操作

避坑指南:我曾经在 Android 9 设备上测试后台扫描,发现扫描间隔被强制拉长到 30 秒以上。后来我改用前台服务 + 定时扫描的方式,才解决了实时性问题。

16.2 Android 10+ 位置限制:为什么扫蓝牙要位置权限?

从 Android 10 开始,扫描蓝牙设备必须拥有 ACCESS_FINE_LOCATION 权限。很多人不理解:我扫个蓝牙耳机,跟位置有什么关系?

其实 Google 的逻辑是:蓝牙信号强度可以用来估算设备距离,从而推断用户位置。说白了,这是隐私保护的一种手段。

我记得有个用户反馈说 App 在 Android 11 上扫描不到任何设备,排查了半天发现是位置权限没弹窗。后来加了权限申请逻辑,问题就解决了。

// Android 10+ 蓝牙扫描权限声明
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Android 12+ 新增 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

注意:Android 12(API 31)进一步细化了蓝牙权限,将扫描和连接权限分开。如果你的 App 只做扫描不做连接,可以只申请 BLUETOOTH_SCAN 权限,不需要 BLUETOOTH_CONNECT

16.3 前台服务保活方案:最稳的保活方式

说到保活,前台服务是目前最靠谱的方案。原理很简单:系统会认为前台服务是用户正在使用的功能,不会轻易杀掉。

我个人习惯的做法是:创建一个 BluetoothScanService,继承 Service,在 onStartCommand 中启动前台通知,然后在服务里执行蓝牙扫描。

public class BluetoothScanService extends Service {
    private static final int NOTIFICATION_ID = 1001;
    private BluetoothLeScanner scanner;
    
    @Override
    public void onCreate() {
        super.onCreate();
        BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        scanner = manager.getAdapter().getBluetoothLeScanner();
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 构建前台通知
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("蓝牙扫描中")
                .setContentText("正在搜索附近设备...")
                .setSmallIcon(R.drawable.ic_bluetooth)
                .build();
        startForeground(NOTIFICATION_ID, notification);
        
        // 开始扫描
        startScan();
        return START_STICKY;
    }
    
    private void startScan() {
        ScanSettings settings = new ScanSettings.Builder()
                .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                .build();
        scanner.startScan(null, settings, scanCallback);
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (scanner != null) {
            scanner.stopScan(scanCallback);
        }
    }
    
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

我的经验:前台服务的通知不能取消,但你可以把通知内容做得友好一些。比如显示当前扫描到的设备数量,或者显示扫描状态。用户看到通知就知道 App 在工作,不会觉得莫名其妙。

16.4 WorkManager 定时扫描:省电又靠谱

如果你的业务场景不需要实时扫描,比如每隔 15 分钟检查一下附近有没有特定设备,那 WorkManager 是最佳选择。

WorkManager 是 Jetpack 的一部分,它会根据系统状态(电量、网络、Doze 模式)来合理安排任务执行时间。说白了,系统会在最合适的时候执行你的扫描任务,既省电又不会被打断。

// 定义 WorkManager 任务
public class BluetoothScanWorker extends Worker {
    public BluetoothScanWorker(@NonNull Context context, @NonNull WorkerParameters params) {
        super(context, params);
    }
    
    @NonNull
    @Override
    public Result doWork() {
        // 执行蓝牙扫描
        BluetoothLeScanner scanner = getBluetoothScanner();
        if (scanner != null) {
            // 扫描逻辑...
            scanDevices(scanner);
        }
        return Result.success();
    }
}

// 设置定时任务:每 15 分钟执行一次
PeriodicWorkRequest scanRequest = 
    new PeriodicWorkRequest.Builder(BluetoothScanWorker.class, 15, TimeUnit.MINUTES)
        .setConstraints(new Constraints.Builder()
            .setRequiresBatteryNotLow(true)
            .build())
        .build();

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "bluetooth_scan",
    ExistingPeriodicWorkPolicy.KEEP,
    scanRequest
);

关键点:WorkManager 的最小间隔是 15 分钟。如果你需要更频繁的扫描,还是得用前台服务。我一般这样选型:

  • 需要实时响应(如门禁、支付)→ 前台服务
  • 定时检查(如设备巡检、签到)→ WorkManager
  • 用户主动触发 → 普通 Service + 前台通知

16.5 综合实战:一个完整的后台扫描方案

说了这么多,我们来整合一下。假设你要做一个蓝牙门禁 App,要求:

  • App 在后台时能持续扫描门禁设备
  • 扫描到设备后自动连接开门
  • 省电,不能把用户手机搞没电

我的方案是这样的:

  1. 权限准备:申请 ACCESS_FINE_LOCATION(Android 10+)和 BLUETOOTH_SCAN(Android 12+)
  2. 前台服务:创建 DoorScanService,启动前台通知
  3. 扫描策略:使用 SCAN_MODE_LOW_POWER 模式,扫描 10 秒停 50 秒,循环执行
  4. 结果处理:扫描到目标设备后,触发连接逻辑
// 循环扫描的伪代码
public class DoorScanService extends Service {
    private static final long SCAN_DURATION = 10_000;  // 扫描 10 秒
    private static final long SCAN_INTERVAL = 50_000;  // 间隔 50 秒
    private Handler handler = new Handler();
    
    private Runnable scanRunnable = new Runnable() {
        @Override
        public void run() {
            startScan();
            // 10 秒后停止扫描
            handler.postDelayed(() -> stopScan(), SCAN_DURATION);
            // 50 秒后再次扫描
            handler.postDelayed(this, SCAN_DURATION + SCAN_INTERVAL);
        }
    };
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        startForeground(NOTIFICATION_ID, buildNotification());
        handler.post(scanRunnable);
        return START_STICKY;
    }
}

踩坑提醒:我曾经在华为手机上测试这个方案,发现手机锁屏后扫描间隔被系统拉长到 2 分钟以上。后来加了 android:foregroundServiceType="connectedDevice" 声明,问题才解决。不同厂商的定制系统会有差异,建议多机型测试。

好了,关于蓝牙后台扫描限制的内容就这些。核心就是:前台服务保活 + 合理扫描间隔 + 权限申请到位。你想想看,只要把这三点做好,大部分场景都能覆盖。

公众号:蓝海资料掘金营,微信deep3321