28、WiFi与后台限制:Android 9+ 后台扫描限制、前台服务与WiFi扫描、WorkManager调度WiFi任务

做WiFi开发这么多年,后台扫描限制这块,可以说是踩坑最多的一个点。Android从9.0开始,对后台扫描的限制越来越严。说白了,Google就是想让App别在后台偷偷摸摸地频繁扫描,省电又省资源。

我刚开始接手一个WiFi定位项目时,就遇到了这个问题。App一退到后台,扫描结果就停了,用户反馈说定位不准。嗯,那时候我才真正意识到,后台扫描限制不是闹着玩的。

Android 9+ 的后台扫描限制

先说说这个限制到底是什么。Android 9(API 28)引入了后台扫描限制,核心规则就两条:

  • App处于后台时,每2分钟最多只能发起4次WiFi扫描
  • 如果App被系统标记为「空闲」状态,扫描频率会更低

为什么会这样?因为每次WiFi扫描都会唤醒WiFi芯片,消耗电量。Google统计过,频繁的后台扫描能让手机续航缩短30%以上。所以这个限制,其实是为了保护用户。

核心要点:后台扫描限制只影响「后台状态」的App。如果你的App在前台,或者有前台服务在运行,扫描频率不受限制。

我记得有一次,测试同事跟我说:「为什么我的测试机扫描正常,你的就不行?」后来发现,他的测试机是Android 8.0,我的已经是Android 10了。嗯,版本差异就是这么坑。

如何判断App是否在后台?

系统判断后台状态,主要看这几点:

  • App没有可见的Activity
  • 没有运行前台服务(Foreground Service)
  • 没有被其他App绑定(Bound Service)

说白了,只要你的App有一个可见的界面,或者有一个前台服务在跑,系统就认为你在前台,扫描限制就不会生效。

前台服务 + WiFi扫描:最常用的方案

我个人习惯的做法是:用前台服务来执行WiFi扫描。这样既保证了扫描频率,又不会触发后台限制。

来看一个简单的实现:

public class WifiScanService extends Service {
    private static final int NOTIFICATION_ID = 1001;
    private WifiManager wifiManager;
    private ScanCallback scanCallback;

    @Override
    public void onCreate() {
        super.onCreate();
        wifiManager = (WifiManager) getApplicationContext()
            .getSystemService(Context.WIFI_SERVICE);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 构建前台通知
        Notification notification = new NotificationCompat.Builder(this, "wifi_channel")
            .setContentTitle("WiFi扫描中")
            .setContentText("正在后台扫描WiFi信号")
            .setSmallIcon(android.R.drawable.ic_menu_search)
            .build();

        startForeground(NOTIFICATION_ID, notification);

        // 开始扫描
        startWifiScan();
        return START_STICKY;
    }

    private void startWifiScan() {
        scanCallback = new ScanCallback() {
            @Override
            public void onScanResults(List<ScanResult> results) {
                // 处理扫描结果
                Log.d("WifiScan", "扫描到 " + results.size() + " 个WiFi");
            }
        };

        wifiManager.registerScanCallback(scanCallback, new Handler());
        wifiManager.startScan();
    }

    @Override
    public void onDestroy() {
        if (scanCallback != null) {
            wifiManager.unregisterScanCallback(scanCallback);
        }
        super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

小提示:前台服务的通知不能取消,用户会一直看到。如果你的App需要长时间后台扫描,建议在通知里显示扫描状态,让用户知道你在干什么。

我在项目中遇到过一个问题:前台服务启动后,扫描确实正常了,但用户反馈说手机发热。后来排查发现,是因为扫描间隔太短,WiFi芯片一直在工作。解决方案是加了一个扫描间隔控制,比如每30秒扫描一次。

WorkManager调度WiFi任务

如果你不需要实时扫描,只是想在特定时间点或者特定条件下执行WiFi扫描,那WorkManager是更好的选择。

WorkManager是Android官方推荐的异步任务调度库,它能自动适配系统限制。比如:

  • App被杀后,任务仍然会执行
  • 系统会合并多个任务,减少唤醒次数
  • 支持延迟执行、周期性执行

来看一个WorkManager调度WiFi扫描的例子:

public class WifiScanWorker extends Worker {
    private WifiManager wifiManager;

    public WifiScanWorker(@NonNull Context context, 
                          @NonNull WorkerParameters params) {
        super(context, params);
        wifiManager = (WifiManager) context
            .getApplicationContext()
            .getSystemService(Context.WIFI_SERVICE);
    }

    @NonNull
    @Override
    public Result doWork() {
        // 检查WiFi是否开启
        if (!wifiManager.isWifiEnabled()) {
            return Result.success();
        }

        // 执行扫描
        boolean scanSuccess = wifiManager.startScan();
        
        if (scanSuccess) {
            List<ScanResult> results = wifiManager.getScanResults();
            // 处理结果,比如保存到数据库
            saveResults(results);
            return Result.success();
        } else {
            return Result.retry();
        }
    }

    private void saveResults(List<ScanResult> results) {
        // 保存扫描结果到本地数据库
    }
}

调度这个Worker也很简单:

// 每30分钟执行一次WiFi扫描
PeriodicWorkRequest scanRequest = 
    new PeriodicWorkRequest.Builder(WifiScanWorker.class, 
                                    30, TimeUnit.MINUTES)
    .setConstraints(new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.NOT_ROAMING)
        .build())
    .build();

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

注意:WorkManager的最小间隔是15分钟。如果你需要更频繁的扫描,还是得用前台服务。另外,WorkManager不保证任务准时执行,系统可能会延迟执行以节省电量。

我曾经在一个项目中,用WorkManager来做WiFi指纹采集。每天凌晨2点,当用户充电且连接WiFi时,自动采集一次WiFi信号数据。这样既不影响用户体验,又能保证数据更新。

三种方案的对比

方案 适用场景 优点 缺点
前台服务 需要实时、高频扫描 不受后台限制,扫描频率高 用户可见通知,耗电较高
WorkManager 定时、条件触发扫描 系统优化,省电,存活率高 最小间隔15分钟,不保证准时
普通后台扫描 不推荐使用 实现简单 受严格限制,扫描频率极低

核心逻辑流程图

WiFi后台扫描方案选择流程 开始:需要后台WiFi扫描 是否需要实时扫描? 前台服务 高频扫描,用户可见 是否定时/条件触发? WorkManager 定时任务,省电 不推荐:普通后台扫描

你想想看,如果你的App只是每隔几小时采集一次WiFi数据,用前台服务就太浪费了。WorkManager完全够用,而且系统会帮你优化调度。反过来,如果你在做实时定位导航,那前台服务就是唯一的选择。

嗯,最后说一句:不管用哪种方案,记得在AndroidManifest.xml里声明权限。WiFi扫描需要ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION权限,而且从Android 10开始,还需要ACCESS_BACKGROUND_LOCATION权限才能在后台扫描。

总结一下:

  • Android 9+ 对后台WiFi扫描有严格限制
  • 前台服务可以绕过限制,适合高频扫描场景
  • WorkManager适合定时、条件触发的扫描任务
  • 普通后台扫描基本不可用,别指望它

我曾经踩过一个坑:在Android 11上,即使用了前台服务,如果用户手动关闭了「位置信息」开关,WiFi扫描也会失败。所以记得在代码里检查位置开关状态,并引导用户开启。


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