5、扫描机制(Scan):ScanSettings 与 ScanResult、扫描触发流程、扫描结果回调

Wi-Fi 扫描,说白了就是手机去「听」一下周围有哪些热点在喊话。这个机制看起来简单,但坑不少。我最早接触 Android Wi-Fi 时,以为扫描就是调个 API 完事,结果发现扫描时机、参数配置、结果回调,每一步都可能翻车。

今天我们就把它彻底拆开来看。

5.1 扫描的核心数据结构

先看两个最基础的类:ScanSettingsScanResult。一个管「怎么扫」,一个管「扫到了啥」。

5.1.1 ScanSettings:扫描参数配置

ScanSettings 用来告诉系统扫描的「姿势」。比如只扫 5GHz、只扫某个频段、要不要被动扫描等。

ScanSettings settings = new ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)  // 低延迟模式
    .setChannelSet(channelList)                        // 指定信道列表
    .build();

常用的扫描模式有三种:

模式 说明 典型场景
SCAN_MODE_LOW_LATENCY 低延迟,快速返回结果 用户主动点击扫描
SCAN_MODE_LOW_POWER 省电,扫描间隔拉长 后台周期性扫描
SCAN_MODE_HIGH_ACCURACY 高精度,全信道扫描 定位场景
我的经验: 低延迟模式虽然快,但耗电也猛。我曾在某个项目中,用户频繁下拉刷新 Wi-Fi 列表,结果手机发烫。后来改成低功耗模式 + 缓存策略,体验好很多。

5.1.2 ScanResult:扫描结果实体

每个 ScanResult 代表一个被扫描到的 AP(接入点)。关键字段如下:

  • SSID:热点名称,比如 "CMCC-5G"
  • BSSID:MAC 地址,用来区分同名热点
  • level:信号强度,单位 dBm,负值越小信号越弱
  • frequency:频率,用来判断是 2.4G 还是 5G
  • capabilities:加密方式,比如 WPA2-PSK
  • timestamp:扫描时间戳,判断结果是否过期
for (ScanResult result : scanResults) {
    String ssid = result.SSID;
    int rssi = result.level;
    String capabilities = result.capabilities;
    // 处理每个热点
}
注意: 同一个 SSID 可能有多个 BSSID(比如商场里的 Mesh 网络)。如果你只按 SSID 去重,可能会漏掉信号更好的那个 AP。

5.2 扫描触发流程

扫描不是你想扫就能扫的。Android 对扫描频率有限制,防止 App 恶意耗电。我见过不少开发者一上来就疯狂调 startScan(),结果被系统限频,啥也扫不到。

5.2.1 应用层触发

标准流程是:

  1. 获取 WifiManager 实例
  2. 注册 ScanResultCallback(Android 12 以上)或 BroadcastReceiver
  3. 调用 startScan()
  4. 等待回调
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);

// Android 12+ 推荐方式
wifiManager.registerScanResultCallback(executor, new WifiManager.ScanResultCallback() {
    @Override
    public void onScanResultAvailable() {
        List<ScanResult> results = wifiManager.getScanResults();
        // 处理结果
    }
});

wifiManager.startScan();

5.2.2 系统层的扫描调度

应用层调 startScan() 后,会走到 WifiScanningServiceImpl。这个服务会做几件事:

  • 检查扫描限频(每个 App 每 30 秒最多一次)
  • 合并多个 App 的扫描请求
  • 下发扫描命令到 HAL 层
  • 等待驱动返回结果
关键点: 系统会合并扫描请求。比如 App A 和 App B 同时请求扫描,实际只扫一次,结果分发给两个 App。这样省电又高效。

5.2.3 扫描限频机制

Android 对扫描频率有严格限制:

限制类型 限制值 说明
单 App 扫描间隔 30 秒 同一个 App 两次扫描至少间隔 30 秒
全局扫描间隔 15 秒 所有 App 合并后,扫描间隔不低于 15 秒
后台扫描限制 更严格 App 在后台时,扫描频率进一步降低
我曾经踩过的坑: 有个 App 在后台每 10 秒扫一次,结果被系统判定为恶意行为,直接拉黑。后来我改成监听系统广播 + 按需扫描,才解决问题。

5.3 扫描结果回调

扫描结果怎么回到应用层?这里有两个路径:

5.3.1 传统 BroadcastReceiver 方式

老项目里常见这种写法:

IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
context.registerReceiver(receiver, filter);

// 在 receiver 中
public void onReceive(Context context, Intent intent) {
    boolean success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
    if (success) {
        List<ScanResult> results = wifiManager.getScanResults();
    }
}

这种方式有个问题:广播是全局的,所有 App 都能收到。而且你没法知道扫描是否真的成功。

5.3.2 Android 12+ 的 ScanResultCallback

新 API 更优雅:

wifiManager.registerScanResultCallback(executor, new WifiManager.ScanResultCallback() {
    @Override
    public void onScanResultAvailable() {
        // 扫描完成,可以取结果了
        List<ScanResult> results = wifiManager.getScanResults();
    }

    @Override
    public void onScanFailure(int reason) {
        // 扫描失败,reason 表示失败原因
    }
});

我个人更喜欢这种方式。回调更精确,而且能区分成功和失败。你想想看,如果扫描失败了你还在那等结果,是不是很尴尬?

5.4 扫描流程全景图

下面这张图把整个扫描链路串起来了:

App 调用 startScan() WifiScanningServiceImpl 限频检查 + 请求合并 HAL / 驱动 下发扫描命令 Wi-Fi 芯片扫描信道 主动扫描 / 被动扫描 扫描结果上报 ScanResult 列表 回调分发 Broadcast / Callback App 收到结果 图例 应用层 框架层 HAL/驱动层

5.5 避坑指南

最后分享几个我实际项目中遇到的坑:

  • 扫描结果为空? 先检查 Wi-Fi 是否开启。很多新手忘了这步。
  • 结果太旧? 检查 timestamp 字段。我建议超过 10 秒的结果就不要用了。
  • 扫描太慢? 看看是不是被限频了。可以用 adb shell dumpsys wifi 查看扫描记录。
  • 回调不触发? 确认是否在 UI 线程注册了 Callback。有些回调要求在主线程。
我的习惯: 每次扫描前先检查 WifiManager.isScanAlwaysAvailable()。如果返回 false,说明系统禁止扫描,这时候调 startScan() 也是白费力气。

嗯,扫描机制就聊到这儿。说白了就是「配置 → 触发 → 回调」三步走,但每一步都有细节。你把这些细节吃透了,Wi-Fi 扫描这块基本就稳了。


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