16、蓝牙后台扫描限制:Android 8+后台限制、Android 10+位置限制、前台服务保活方案、WorkManager定时扫描
做蓝牙开发这么多年,后台扫描这块坑是真的多。我记得刚接手一个蓝牙门禁项目时,用户反馈说手机锁屏几分钟后就扫描不到设备了。我一开始还以为是硬件问题,查了半天才发现——嗯,是系统把我们的扫描给掐了。
从 Android 8 开始,Google 对后台扫描的限制越来越严。说白了,系统觉得你一个 App 在后台偷偷扫描蓝牙,既耗电又侵犯隐私。但咱们做 IoT 或室内定位的,后台扫描又是刚需。怎么办?今天我就把这几年的踩坑经验掰开揉碎讲给你听。
核心结论:Android 8+ 限制后台扫描频率,Android 10+ 要求位置权限,前台服务是目前最稳的保活方案,WorkManager 适合非实时性扫描任务。
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 在后台时能持续扫描门禁设备
- 扫描到设备后自动连接开门
- 省电,不能把用户手机搞没电
我的方案是这样的:
- 权限准备:申请
ACCESS_FINE_LOCATION(Android 10+)和BLUETOOTH_SCAN(Android 12+) - 前台服务:创建
DoorScanService,启动前台通知 - 扫描策略:使用
SCAN_MODE_LOW_POWER模式,扫描 10 秒停 50 秒,循环执行 - 结果处理:扫描到目标设备后,触发连接逻辑
// 循环扫描的伪代码
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